The following doesn't give you a step-by-step guide of what to do. It gives you a number of individual pieces and you need to put them together in order to meet the main feature requirement of the Messages Blade:
Display message details that are received from the Chat Service
The Messages Blade uses two services which will enable you to achieve this functionality:
We're nearly ready to start coding. But before we do let's do a little bit of setup.
As we said, a nice team have put together some fake services to help us develop our Blade's functionality. In order to configure the services you need to know the require path to these implementations.
Open the aliases.xml
configuration file for the Workbench,
blades/messages/workbench/resources/aliases.xml
, and set the content as:
<aliases xmlns="http://schema.caplin.com/CaplinTrader/aliases" useScenario="dev">
<alias name="chat.service" class="chatservice.FakeChatService"/>
</aliases>
Yes, it's XML. What can we say! Anyway, we've now actually configured:
chat.service
to use a fake chat service implementationNow that the helper service is in place, we can get to work.
In order to get the Chat Service we first need to get the ServiceRegistry
.
Update your MessagesViewModel
accordingly e.g.
var ServiceRegistry = require( 'br/ServiceRegistry' );
To get the list of existing messages we need to retrieve the Chat Service
from the ServiceRegistry
.
this._chatService = ServiceRegistry.getService( 'chat.service' );
The Chat Service exposes a getMessages
function that takes a listener
.
this._chatService.getMessages( listener );
The listener should implement two callback functions; messagesRetrieved
and
messageRetrievalFailed
. This can be achieved by passing in an object literal that defines these functions,
or by making the MessagesViewModel
implement them and passing in this
as the listener.
We've found that although passing in listeners that have to implement contracts
(in the same way as services do) can be a bit more effort, it can result in
a much more robust solution. There are of course times where passing in a function
will be fine.
Here's how you make the MessagesViewModel
fulfil the listener contract:
MessagesViewModel.prototype.messagesRetrieved = function( messages ) {
// add the messages to the `messages` View Model Array
};
MessagesViewModel.prototype.messageRetrievalFailed = function() {
// Something has gone wrong.
};
From there you need to add the messages to the messages
ObservableArray.
Once this is complete you should see a full list of existing messages in the Workbench UI.
You'll remember that when we were focusing on just building the MessagesViewModel
we added calls to viewModel.addMessage
in the Workbench. Now we want to make
sure that the View Model is interacting with the Chat Service. So, in the Workbench
you can replace the calls to viewModel.addMessage
with code that fetches
the Chat Service and calls sendMessage
on it; this will store the messages so that
calls to getMessages
return those messages.
The code looks something like this:
this._chatService = ServiceRegistry.getService( 'chat.service' );
this._chatService.sendMessage( { userId: 'testUser', text: 'Word Up!', timestamp: new Date() } );
The Chat Service emits a new-message
event whenever a new message becomes available.
So, in order to be informed when that happens you need to bind to that event on
the Chat Service.
You should only bind to this event once you have the initial list of messages.
This avoids accidentally adding newer messages before the older ones, dealing with
duplicates and reordering. This can be done in messagesRetrieved
.
An example of doing this, calling a function on the MessagesViewModel
and maintaining
the this
context is:
// do this within an instance function
this._chatService.on( 'new-message', this.handleNewMessage, this );
As part of developing the Chat Service one of the teams added a Chat Workbench Tool.
This can be added to the Workbench really easily. Just add the following to the end
of blades/messages/index.html
:
function addChatTool(workbench ) {
var ChatWorkbenchTool = require( 'chatservice/ChatWorkbenchTool' );
var tool = new ChatWorkbenchTool();
workbench.addToLeftWing( tool, "Chatting", false);
}
addChatTool( workbench );
You can see the code required for this in libs/chatservice/
...:
src/chatservice/ChatWorkbenchTool.js
- Creates a UI component and adds to the Workbenchresources/workbench-chat-tool.html
- The HTML templateDue to the Services architecture it really is that simple to add these developer tools.
You can use the sendMessage
function we used earlier to test sending messages
from the JavaScript console e.g.
var chatService = ServiceRegistry.getService( 'chat.service' );
chatService.sendMessage(
{ userId: 'testUser', text: 'Awesome console message!', timestamp: new Date() }
);
The team that is building the User Card Blade rely on being informed if a user
is selected. This is achieved by other components within ModularApp broadcasting
user-selected
events on a user
channel.
For the Messages Blade this means:
class="message-user-id"
MessagesViewModel
or the MessageItemViewMode
-
see KnockoutJS binding contextuserId
property that identifies the user that was selecteduser-selected
event on a user
channel on the EventHub
The EventHub can be retrieved from the ServiceRegistry
as follows:
var ServiceRegistry = require( 'br/ServiceRegistry' );
this._eventHub = ServiceRegistry.getService( 'br.event-hub' );
Channels can be retrieved from the EventHub:
var channel = this._eventHub.channel( 'user' );
Events are triggered on the Channel:
channel.trigger( 'user-selected', { userId: 'testUser' } );
Over to you.
No hints here. Any questions, please ask.
Before we start, you need to copy the contents of aliases.xml
from the Workbench
into messages/test-unit/resources/aliases.xml
.
In the services overview we talked about how using MVVM and Services allows us to test full features in isolation. In this part of the exercise we're going to do exactly that. We'll demonstrate how to achieve two types of test:
This way we're testing how UI interactions result in service interactions and how service events are reflected in UI state.
Let's start by creating a new file called MessagesFeatureTest.js
in messages/test-unit/tests/
and update it to look as follows to add the Test Suite:
'use strict';
require( 'jasmine' );
var MessagesViewModel = require( 'modularapp/messages/MessagesViewModel');
var ServiceRegistry = require( 'br/ServiceRegistry' );
describe( 'The Messages', function() {
} );
Note: You could call this file MessagesSpecTest.js.
The first thing to notice is that we're using Jasmine, and specifcially we're using Jasmine 1.3 as it ships with BRJS.
As with anything in software it's possible to achieve the same thing in multiple ways. For example, we can test service interactions by:
ServiceRegistry
with a Mock objectSince we're using Jasmine, we'll use Spies, but we'll also demonstrate how the Fake service, that was developed really to help our development within the Workbench, is also useful here.
The only user interaction that takes place in the Messages Blade is the user
clicking on the User ID in a message. This results in an interaction with the
EventHub
service.
Add the following spec to the The Input
suite:
describe( 'The Messages', function() {
it( 'Should trigger a "user-selected" event on a user channel on the EventHub when a user is selected', function() {
var eventHub = ServiceRegistry.getService( 'br.event-hub' );
spyOn( eventHub, 'channel' ).andCallThrough();
spyOn( userChannel, 'trigger' );
var testUserId = 'testUser';
// TODO: Interact with View Model as if the user has selected a User Id
var expectedEventData = {
userId: testUserId
};
expect( eventHub.channel ).toHaveBeenCalledWith( 'user' );
expect( userChannel.trigger ).toHaveBeenCalled( 'user-selected', expectedEventData );
} );
} );
The test above is nearly complete. You just need to interact with the MessagesViewModel
and simulate the user clicking the User Id. You'll need to make sure that the appropriate
data (the User Id) is passed to the click handler.
testUserId
value in the data that's passed to the click handler as
it's also used when creating the expectedEventData
object used in the assertionThe next thing we want to do is see how services can impact the state of the View Model.
To do this we'll use the chat service - we've seen that our FakeChatService
implementation triggers a new-message
event when the sendMessage
function is
called. We can use this knowledge to do the test.
Before we start, we need to remember that our implementation doesn't bind to the
new-message
event until it's recieved the getMessages
callback which is an
asynchronous call. Luckily our FakeService
has a setting FakeService.fakeAsync
so that it doesn't make this callback truly asynchronously. This means we can ensure
that the getMessages
response has been made before we call sendMessage
.
Here's the test template - just fill in the blanks:
it( 'Should display new messages that are send via the Chat Service', function() {
// Setup
var chatService = ServiceRegistry.getService( 'chat.service' );
chatService.fakeAsync = false;
var messagesViewModel = new MessagesViewModel();
var message = { userId: 'testUserId', text: 'testUserText', timestamp: new Date() };
// Execute
// TODO: send message using the Chat Service
// Assert
var firstMessage = messagesViewModel.messages()[ 0 ];
// TODO: expect( ... );
} );
FakeChatService.fakeAsync = false;
is to use the very
handy jasmine.Clock.FakeChatService.fakeAsync
after the test.
Otherwise future tests may be affected by this unexpected synchronous behaviour.Our Blade is now interacting with two services and we've tested both UI through to Services and Services through to the UI; full feature testing.
It's time to commit those changes and push them to github:
git add blades/messages
git commit -m 'integrating messages blade with services'
git pull origin master
git push origin master
See how the other teams in your company are getting on. Can you help them out?
Then, we'll see how our application looks.