Lab 4 Part b

We continue our work on the assignment with the overall index.html structure, and then the Scorecard UI.

Overall structure

What we want to do now is set up a bit of our overall page layout. Here is our overall plan:

Before moving on, make a new issue in GitHub titled “overall page layout” and list the above three items in a comment. Add it to your milestone.

So the top level interface could be written in HTML as follows (add these inside the body tag in the index.html page, and before the script section there):

   <div id="scorecard"></div>
   <div id="doors"></div>
   <div id="messages"></div>

We will obviously need to format them later once we have something more to show. For now let’s leave it at that. Make a commit and link it to your issue about the UI. We will return to it later.

Scorecard UI

The ScorecardUI class is responsible for setting up the HTML for the scorecard, along with any needed jQuery methods to react to user actions, and methods to update the values when the score is updated. Our goal is for this class to not be directly related to the Score class itself. There will therefore be some amount of duplicated information.

This class will not be built in a test-driven-development style, as it involves the UI. What we will do is inspect the UI every few steps, to make sure things look OK. We will do this in a copy of the index.html file, which we will call testUI.html. The idea is that both files will be loading the same modules, but the UI file will have extra stuff that we can use to examine the behavior.

Create such a copy now, at the same level as the index.html, and open it in the browser.

Let’s start by making an issue about building the scorecard UI (give it the scorecard label and add it to the milestone). And let’s add some comments to it:

- The scorecard UI class will be given DOM element as a parameter, and it will store it.
- When we use this class in our application, this element will be the div with id "scorecard".
- The class needs to add labels and values for the four counters we wish to report, probably in two sections, one for "SWITCH" and one for "STAY". A method called `initialize` would be given those values, and it will use string templates and jQuery to add the appropriate HTML to the page.
- The class needs to have an `update` method that is given some information about which value is to be updated, and updates it accordingly. We might set up a highlight of the value on a timer, to make it stand out.
- The class will be managing a "Reset" button, and when the button is pressed it triggers a "reset" event. Our controller will later be listening to that event, therefore we will need to make our class follow the Observer pattern. A class called `Observable` has been provided for us, and our UI class will need to extend it.

We start by setting up the basics of the ScorecardUI class.

This is a good time to make a commit, referring to initial UI for the ScorecardUI class. Make sure you link it to the corresponding issue number!

We will at some point worry about the look-and-feel of the whole thing, but for now let us focus on functionality. The class needs to have an update method that is given simply the name of the updated field, for example "switchWins", and an updated value. It is then responsible for updating a corresponding value on the table. Let’s work on creating this method.

The reset button and observables

One last thing we need to set up is what happens when the “Reset” button is clicked. Of course the values on our Score model will need to be reset, but our little UI doesn’t know anything about the Score class, and we like it that way. What the UI will need to do when the Reset button is clicked, is send out an announcement, and hope someone is listening. Therefore we would like to use the observer pattern for that.

And this concludes our ScorecardUI class! We’ll work on the CSS later. For now, make a final commit and close the issue.

Notice how our ScorecardUI class had very little real logic in it. It is simply there to protect the rest of the application from knowing about specific UI details. But we don’t want it to be too smart:

Next up we will work on the ScorecardController class, which will coordinate between the Score model and the ScorecardUI view.

ScorecardController

Now it is time to work on the Scorecard controller. Start by creating an issue:

Scorecard controller is the interface between the Score model and the ScorecardUI class. It knows about both of them and coordinates between them.

- Its constructor would be taking as input two things: A UI instance and a score model. We will use a mock UI instance in our testing. It will call the UI's initialize method at the start.
- It will register to listen for a change event on the Score model. We'll need to make the Score model observable. It will then communicate the corresponding changes to the UI.
- It will register to listen to a reset change from the UI, then pass that reset instruction on to the Score model.

Let’s get started!

Our first controller test is in theory going to be relatively easy: We just need to make sure that the controller registers to listen to score changes. For this we’ll need a few ingredients. First, we’ll need to turn the Score class into an observable, and add some tests to that effect. And those test will take us down the rabbit hole of asynchronous testing.

Let’s start by turning the Score class into an observable. Do this now, by following the same steps we did for ScorecardUI, namely importing the Observable class and then making the Score class definition extend Observable, and adding a call to super() in the constructor. Do this now and make sure your tests still pass.

We still haven’t set up the class to trigger anything, but let’s work through our first test for this. Go back to the Score class test file, test/score.spec.js.

Asynchronous events and testing

Let’s discuss the problem we will need to face with these tests. If you look at the Observable class, you will notice that it triggers its events asynchronously: It creates timeouts for the events to execute once the current function is done. This presents some challenges. For instance imagine that we add our next test as follows:

it('trigger messages when values change (switchWins)', () => {
   let h = (msg, value) => {
      expect(msg).to.equal('switchWins');
      expect(value).to.equal(2);
   };
   score.on('change', h);
   score.trigger('change', 'switchWins', 1);
});

So what we do here is prepare a function h, then we give it as a handler to the observable. Then we call trigger, and expect that the function h will be called and will do the two checks listed there. (actually in practice we would not call trigger ourselves, but we haven’t hooked that up yet). Put that test in and run that code and see what happens. This code should be failing, but you’ll see that the tests technically all pass (even though you see an uncaught exception pop up on the console).

Note that we purposefully wrote this test with the wrong value in the trigger call; this test should be failing, but it does not fail in a normal way, and we need to fix that.

So what is going on here? The problem is that when trigger happens, the observable class sets up a call to occur in the future, and in the meantime it returns. The flow of control will then go to the end of the it, which will finish normally and therefore the test will register as “done” and succeeding. So the problem is that our function h never gets a chance to run until after the it call has been completed.

In order to deal with these problems, the one solution is to use some mock observable structure that works synchronously rather than asynchronously.

The other approach, which we will take, is to accept that the operations will happen asynchronously, and to adapt the tests accordingly.

Mocha offers us an important tool in this process, via the done parameter. Our tests will look something like this:

it('...', (done) => {
   ... do stuff ...
   ... possibly asynchronously ...
   ... call done()  when the test is trully done ...
});

The idea of this is that the system does not consider the task completed until that function done is called. It wait wait for asynchronous events to complete if they are the ones that trigger the done. For example our test could look as follows:

it('trigger messages when values change (switchWins)', (done) => {
   let h = (msg, value) => {
      expect(msg).to.equal('switchWins');
      expect(value).to.equal(2);
      done();
   };
   score.on('change', h);
   score.trigger('change', 'switchWins', 1);
});

Run this test instead and see how it fails a bit more gracefully. Now comment out the trigger line and see what happens: The test will time out and get reported as failing if done is never called.

In order to understand this better, take a look at the test/observable.spec.js file, where done is used extensively.

Now on to our Score tests. Replace this test you just put with the following:

it('trigger messages when values change (switchWins)', (done) => {
   let h = (msg, value) => {
      expect(msg).to.equal('switchWins');
      expect(value).to.equal(1);
      done();
   };
   score.on('change', h);
   score.addResult(Score.ACTION_SWITCH, Score.RESULT_WIN);
});

This test should fail, because we have not done anything in addResult that would trigger an event. So run your tests and you should see this test time out after 2 seconds.

Now we need to make this test pass. If you look in your addResult function, you can imagine various places where we can add this kind of trigger. But if we are not careful, we’ll add with 15 different trigger calls, when we really should have one: whenever any of the score value changes, we should have a trigger.

What this means is that we should create a little set function that sets a value. And this function will set the value and then call trigger. And all other places that need to set a value will go through set.

Let’s see how we can make that happen.

Now that we have the Score class all in order, time to return to the ScoreController class and its tests.

Now, the first test we wanted to write for the ScoreController is simply a test that the controller signs up to be notified when values change.

This might be a good time to make a commit. Make sure to reference the appropriate issue number in your message!

Now it is time for our first “real” test on the controller. We need to make sure that when the model triggers a change event, then the controller calls the UI’s update method with the correct arguments. We can easily spy on the update method. But the problem we have is that the controller will respond to the trigger event asynchronously. We therefore want to use the done parameter to control when the test ends. We’ll also enhance our spy with an extra function parameter to call when the function it spies on is getting called. Take a look at our spy.js code to see how that third parameter behaves.

Here’s how that all looks like, add it to the scoreController test file:

it('call the ui update when the model triggers a change', done => {
    let spy = new Spy(ui, 'update', (field, value) => {
        expect(field).to.equal('switchWins');
        expect(value).to.equal(score.switchWins);
        done();
    });
    score.trigger('change', 'switchWins', score.switchWins);
});

So we set up a spy on the update method, and we also provide a third “callback” function that should be called instead of the actual update method. That callback checks that update was given the correct arguments by the controller, and then calls done to end the test.

Run this test now and you should see it time out.

To make the test pass, we simply need to finally implement the modelChanged method of the controller, by adding the following method to the ScoreController class:

modelChanged(property, value) {
   this.ui.update(property, value);
}

You should see your tests pass after this.

In order to wrap up our controller, we need one final component. We need for our controller to set up to listen to the ui for a resetRequested trigger, and call the model’s reset method when that happens. Here is an overview of the steps you will need to implement for this:

And this concludes our ScoreController class and the overall “Scorecard” component! Make sure you make a commit, close the corresponding issue, and check off the corresponding box in our master list.

This concludes part b of the lab. Due to its size, the lab is broken into pieces. Continue to part c