Tải bản đầy đủ (.pdf) (10 trang)

Phát triển Javascript - part 46 ppsx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.38 MB, 10 trang )

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

×