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 EventHubThe 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/messagesgit commit -m 'integrating messages blade with services'git pull origin mastergit push origin masterSee how the other teams in your company are getting on. Can you help them out?
Then, we'll see how our application looks.