ptg 15.5 The Message Form 423 15.5.2.1 Refactoring: Extracting the Common Parts We will take a small detour by refactoring the user form controller. We will ex- tract a formController object from which both of the controllers can in- herit. Step one is adding the new object, as Listing 15.66 shows. Save it in src/ form _ controller.js. Listing 15.66 Extracting a form controller (function () { if (typeof tddjs == "undefined") { return; } var dom = tddjs.dom; var chat = tddjs.namespace("chat"); if (!dom || !dom.addEventHandler || !Function.prototype.bind) { return; } function setView(element) { element.className = "js-chat"; var handler = this.handleSubmit.bind(this); dom.addEventHandler(element, "submit", handler); this.view = element; } chat.formController = { setView: setView }; }()); To build this file, I simply copied the entire user form controller and stripped out anything not related to setting the view. At this point, you’re probably wondering “where are the tests?”. It’savalidquestion. However, we are not adding or modifying behavior, we’re merely moving around parts of the implementation. The existing tests should suffice in telling us if the refactoring is successful—at least for the documented/tested behavior, which is the only behavior we’re concerned about at this point. Step two is making the user form controller use the new generic controller. We can achieve this by popping it in as the form controller’s prototype object, as seen in Listing 15.67. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 424 TDD and DOM Manipulation: The Chat Client Listing 15.67 Changing userFormController’s ancestry chat.userFormController = tddjs.extend( Object.create(chat.formController), util.observable ); Running the tests confirms that this change does not interfere with the exist- ing behavior of the user form controller. Next up, we remove userFormCon- troller’s own setView implementation. The expectation is that it should now inherit this method from formController thus the tests should still pass. Run- ning them confirms that they do. Before the refactoring can be considered done, we should change the tests as well. The tests we originally wrote for the user form controller’s setView should now be updated to test formController directly. To make sure the user form controller still works, we can replace the original test case with a single test that veri- fies that it inherits the setView method. Although keeping the original tests better documents userFormController, duplicating them comes with a maintenance cost. I’ll leave fixing the test case as an exercise. 15.5.2.2 Setting messageFormController’s View Having extracted the formController, we can add a test for messageForm- Controller expecting it to inherit the setView method, as Listing 15.68 shows. Listing 15.68 Expecting messageFormController to inherit setView (function () { var messageController = tddjs.chat.messageFormController; var formController = tddjs.chat.formController; TestCase("FormControllerTestCase", { /* */ "test should inherit setView from formController": function () { assertSame(messageController.setView, formController.setView); } }); }()); Passing the test is achieved by changing the definition of the controller, as seen in Listing 15.69. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.5 The Message Form 425 Listing 15.69 Inheriting from formController chat.messageFormController = Object.create(chat.formController); 15.5.3 Publishing Messages When the user submits the form, the controller should publish a message to the model object. To test this we can stub the model’s notify method, call handle- Submit, and expect the stub to be called. Unfortunately, the controller does not yet have a setModel method. To fix this, we will move the method from user- FormController to formController. Listing 15.70 shows the updated form controller. Listing 15.70 Moving setModel /* */ function setModel(model) { this.model = model; } chat.formController = { setView: setView, setModel: setModel }; Having copied it over, we can remove it from userFormController.To verify that we didn’t break anything, we simply run the tests, which should be all green. To our infinite satisfaction, they are. There is no setModel related test to write for messageFormController that can be expected to fail, thus we won’t do that. We’re TDD-ing, we want progress, and progress comes from failing tests. A test that can push us forward is one that expects the controller to have a handleSubmit method, which can be seen in Listing 15.71. Listing 15.71 Expecting the controller to have a handleSubmit method "test should have handleSubmit method": function () { assertFunction(messageController.handleSubmit); } Listing 15.72 passes the test by adding an empty function. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 426 TDD and DOM Manipulation: The Chat Client Listing 15.72 Adding an empty function function handleSubmit(event) {} chat.messageFormController = Object.create(chat.formController); chat.messageFormController.handleSubmit = handleSubmit; With the method in place we can start testing for its behavior. Listing 15.73 shows a test that expects it to publish a message event on the model. Listing 15.73 Expecting the controller to publish a message event TestCase("FormControllerHandleSubmitTest", { "test should publish message": function () { var controller = Object.create(messageController); var model = { notify: stubFn() }; controller.setModel(model); controller.handleSubmit(); assert(model.notify.called); assertEquals("message", model.notify.args[0]); assertObject(model.notify.args[1]); } }); Listing 15.74 adds the method call to pass the test. Listing 15.74 Calling publish function handleSubmit(event) { this.model.notify("message", {}); } Tests are all passing. Next up, Listing 15.75 expects the published object to include the currentUser as its user property. Listing 15.75 Expecting currentUser as user TestCase("FormControllerHandleSubmitTest", { setUp: function () { this.controller = Object.create(messageController); this.model = { notify: stubFn() }; this.controller.setModel(this.model); }, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.5 The Message Form 427 /* */ "test should publish message from current user": function () { this.model.currentUser = "cjno"; this.controller.handleSubmit(); assertEquals("cjno", this.model.notify.args[1].user); } }); Once again, we extracted common setup code to the setUp method while adding the test. Passing the test is accomplished by Listing 15.76. Listing 15.76 Including the current user in the published message function handleSubmit(event) { this.model.notify("message", { user: this.model.currentUser }); } The final piece of the puzzle is including the message. The message should be grabbed from the message form, which means that the test will need to embed some markup. Listing 15.77 shows the test. Listing 15.77 Expecting the published message to originate from the form TestCase("FormControllerHandleSubmitTest", { setUp: function () { /*:DOC element = <form> <fieldset> <input type="text" name="message" id="message"> <input type="submit" value="Send"> </fieldset> </form> */ /* */ this.controller.setView(this.element); }, /* */ "test should publish message from form": function () { var el = this.element.getElementsByTagName("input")[0]; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 428 TDD and DOM Manipulation: The Chat Client el.value = "What are you doing?"; this.controller.handleSubmit(); var actual = this.model.notify.args[1].message; assertEquals("What are you doing?", actual); } }); To pass this test we need to grab the first input element and pass its current value as the message. Listing 15.78 shows the required update to handleSubmit. Listing 15.78 Grabbing the message function handleSubmit(event) { var input = this.view.getElementsByTagName("input")[0]; this.model.notify("message", { user: this.model.currentUser, message: input.value }); } The tests now pass, which means that the chat client should be operable in a real setting. As before, we haven’t implemented much error handling for the form, and I will leave doing so as an exercise. In fact, there are several tasks for you to practice TDD building on this exercise: • Form should prevent the default action of submitting it to the server • Form should not send empty messages • Add missing error handling to all the methods • Emit an event (e.g. using observable) from the message once a form is posted. Observe it to display a loader gif, and emit a corresponding event from the message list controller when the same message is displayed to remove the loading indicator. I’m sure you can think of even more. 15.5.4 Feature Tests Because most of the functionality is taken care of by the generic form controller, there isn’t much to feature test. The only direct dependencies are tddjs, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.6 The Final Chat Client 429 formController and getElementsByTagName. Listing 15.79 shows the fea- ture tests. Listing 15.79 Feature testing messageFormController if (typeof tddjs == "undefined" || typeof document == "undefined") { return; } var chat = tddjs.namespace("chat"); if (!chat.formController || !document.getElementsByTagName) { return; } /* */ 15.6 The Final Chat Client As all the controllers are complete, we can now piece together the entire chat client and take it for a real spin. Listing 15.80 adds the message form to the HTML document. Listing 15.80 Adding the message form to index.html <! > <dl id="messages"></dl> <form id="messageForm"> <fieldset> <input type="text" name="message" id="message" autocomplete="off"> </fieldset> </form> <! > Copy over message _ form _ controller.js along with form _ controller.js and the updated user _ form _ controller.js and add script elements to index.html to include them. Then update the boot- strap script, as seen in Listing 15.81. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 430 TDD and DOM Manipulation: The Chat Client Listing 15.81 Final bootstrapping script /* */ userController.observe("user", function (user) { /* */ var mForm = document.getElementById("messageForm"); var messageFormController = Object.create(c.messageFormController); messageFormController.setModel(model); messageFormController.setView(mForm); model.connect(); }); </script> Firing up the client in a browser should now present you with a fully functional, if not particularly feature rich, chat client, implemented entirely using TDD and JavaScript, both server and client side. If you experience trouble posting messages, make sure you completed messageFormController by making its handle- Submit method abort the default event action. 15.6.1 Finishing Touches To get a feeling of how the chat application behaves, try inviting a friend to join you over the local network. Alternatively, if you’re alone, fire up another browser, or even just another tab in your current browser. There are currently no cookies involved, so running two sessions from different tabs in the same browser is entirely doable. 15.6.1.1 Styling the Application An unstyled webpage is a somewhat bleak face for the chat application. To make it just a tad bit nicer to rest our eyes on, we will add some CSS. I am no designer, so don’t get your hopes up, but updating css/chapp.css with the contents of Listing 15.82 will at least give the client rounded corners, box shadow, and some light grays. Listing 15.82 “Design” for the chat client html { background: #f0f0f0; } form, dl { display: none; } .js-chat { display: block; } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.6 The Final Chat Client 431 body { background: #fff; border: 1px solid #333; border-radius: 12px; -moz-border-radius: 12px; -webkit-border-radius: 12px; box-shadow: 2px 2px 30px #666; -moz-box-shadow: 2px 2px 30px #666; -webkit-box-shadow: 2px 2px 30px #666; height: 450px; margin: 20px auto; padding: 0 20px; width: 600px; } form, fieldset { border: none; margin: 0; padding: 0; } #messageForm input { padding: 3px; width: 592px; } #messages { height: 300px; overflow: auto; } 15.6.1.2 Fixing the Scrolling As we noted earlier, the client eventually gains a scroll and adds messages below the fold. With the updated stylesheet, the scroll is moved to the definition list that contains the messages. In order to keep the message form visible, we put a restraint on its height. Because we’re more interested in new messages popping in, we will tweak the message list controller to make sure the definition list is always scrolled all the way to the bottom. We can scroll the list to the bottom by setting the scrollTop property to its maximum value. However, we don’t need to determine this value exactly; all we need to do is set it to some value equal to or greater than the max value, and the browser will scroll the element as far as possible. The scrollHeight of an element seems From the Library of WoweBook.Com Download from www.eBookTM.com ptg 432 TDD and DOM Manipulation: The Chat Client like a good fit; its value is the entire height of the element’s contents, which will obviously always be greater than the greatest possible scrollTop. Listing 15.83 shows the test. Listing 15.83 Expecting the message list controller to scroll its view down TestCase("MessageListControllerAddMessageTest", { /* */ "test should scroll element down": function () { var element = { appendChild: stubFn(), scrollHeight: 1900 }; this.controller.setView(element); this.controller.addMessage({ user:"me",message:"Hey" }); assertEquals(1900, element.scrollTop); } }); This test uses a stubbed element rather than the actual element available in the test. In a test such as this, we need complete control over the input and output to verify its correctbehavior. We cannot stub an element’s scrollTop property setter; neither can we easily determine that its value was set correctly, because it depends on the rendered height and requires styles to be added to make the element scroll on overflow to begin with. To pass the test we assign the value of scrollHeight to scrollTop as seen in Listing 15.84. Listing 15.84 Scrolling the message list down on each new message function addMessage(message) { /* */ this.view.scrollTop = this.view.scrollHeight; } 15.6.1.3 Clearing the Input Field When a user has posted her message, it is unlikely that they would like to start the next message with the text from the previous one. Thus, the message form controller should clear the input field once the message is posted. Listing 15.85 shows the test. From the Library of WoweBook.Com Download from www.eBookTM.com . solid #333; border-radius: 12px; -moz-border-radius: 12px; -webkit-border-radius: 12px; box-shadow: 2px 2px 30px #666; -moz-box-shadow: 2px 2px 30px #666; -webkit-box-shadow: 2px 2px 30px #666; height:. Extracting the Common Parts We will take a small detour by refactoring the user form controller. We will ex- tract a formController object from which both of the controllers can in- herit. Step one. method, call handle- Submit, and expect the stub to be called. Unfortunately, the controller does not yet have a setModel method. To fix this, we will move the method from user- FormController to