Well, it’s been a few weeks since I’ve had enough time to sit down at write another post, but here I am at it again. However, for my latest post, I’ve decided to write about a topic in SproutCore that sits outside of the more mainstream features of the framework: that is event simulation.

Now, now — hold on. I see you reaching for you mouse ready to checkout some other website, but hear me out on this topic. While event simulation in SproutCore isn’t really promoted much, it is useful to know. How? When you want to do test automation.

A Little Background

With the advent of modern dynamic web pages, developers and web designers are free to construct web pages that allow users to interact with any part of a web page based on various types of user input. Typically, user input on a web page involves actions using a mouse and keyboard.

A web browser will process triggered events and invoke any registered event handlers to perform some action based on the event received. However, all browsers perform event processing and event dispatching in slightly different ways thereby causing headaches for anyone attempting to develop a consistent user experience.

Various light-weight frameworks have been created over the years to handle these browser inconsistencies and allow developers to work with a standard set of functions to handle raised events and even trigger events. The primary example of this would be, of course, jQuery. But, as you may suspect, the SproutCore framework also has mechanisms to deal with event processing, event dispatching, event normalization, and even triggering events.

Knowing that SproutCore has all of this wonderful eventy stuff, just what exactly is responsible for it? Good question. The answer starts primarily in two objects. They are SC.Event and SC.RootResponder.

SC.Event, which you can find in the event.js file under the foundation/system folder, is mainly responsible for the following:

  • Creating normalized events
  • Adding and removing event handlers
  • Notifying event handlers when the browser raises an event

In addition, SC.Event also allows you to simulate events and trigger them. Basically, SC.Event does a lot of what you find in frameworks like jQuery. Does that mean you are responsible for registering your own event handlers to listen for raised events from the browser? Well, no. I mean, yes, you can, in some special cases it makes sense, but event handling and dispatching really starts with the SC.RootResponder.

At first mention of the SC.RootResponder, it comes across as this mystical thing. SproutCore uses it, but for what? Only leprechauns and elves really know. (I kid… or do I? No, I kid).

SC.RootResponder, in the simplest sense, is the first object (the root object) in SproutCore to handle (or, rather, respond to) a raised event sent from SC.Event to then be further processed and forwarded on for other objects to respond to in a carefully choreographed way.

Where SC.Event normalizes the data of all incoming events from the web browser, SC.RootResponder normalizes an event’s type. What does that mean? Well, events for typing a key, clicking a mouse button, moving a mouse, double clicking, and so on can vary from one browser to the other. SproutCore takes control and boils the incoming events down to a set of standard event types that your objects can work with. In addition, this means SproutCore also has a lot more control of how an application will behave and even let you do cool stuff like drag-and-drop — Whee!

After the root responder is finished processing the incoming event, it then sends the event on to a pane. A pane (SC.Pane) is a special view that has no parent view. The pane then passes the given event up a responder chain so that each responder in the chain has a chance to act on the event if it so chooses to.

During a SproutCore application’s setup, one root responder is created and adds itself as a listener to a bunch of different events using SC.Event. Based on that initial setup, there are actually very few registered event handlers. So, in turn, there ends up being a performance advantage, but I digress.

If you’re interested in looking at the SC.RootResponder code, there are two files to look at. One is the base root responder and the other is the more specific desktop root responder. (Yes, there is a mobile root responder too, but I’ll just focus on desktop based apps). You can find the base root responder in the root_responder.js file under the foundation/system directory. The desktop root responder is in the root_responder.js file under the desktop/system directory.

Simulating Events for Test Automation

With the knowledge of SC.Event and SC.RootResponder now under our collective belts, how does this translate into test automation? The answer is in the objects you create, such as views, that respond to various incoming events. To makes things easier, let’s just focus on views.

As you develop your SproutCore-based application, you will end up using stock views that come with the SproutCore framework, and, depending on the needs of your app, even make your own custom views. While views are nice by themselves, they don’t really offer much utility until integrated with other parts of the system, such as with controllers and model objects. Of course, any good development team won’t just create views and integrate them into the app without tests to assure everything is working as expected.

Testing a view by itself typically means that you are checking the following:

  • That it visualizes itself correctly based on its current state
  • That it correctly updates its state based on incoming UI events

To put it more simply: We are testing a view’s appearance and behavior. As a simple example of testing a view, let’s look at the humble SC.ButtonView.

Starting Simple

The SC.ButtonView is pretty straightforward in how it’s used in its most basic form. You simply have to assign the view a title, a target, and an action. The title will appear centered in the button when it is rendered in the browser. When clicked, the button will respond to the event by invoking the action on the given target. Let’s say we have the following button setup in our app:


myButton: SC.Button.design({
  layout: { ... },
  title: "Click Me",
  target: MyApp.someController,
  action: 'fireAction'
})

As an initial test, we would check that the button does indeed display “Click Me” when rendered using the view’s core query object. However, since we are talking about events in this blog post, we’re more interested in the other test that will cause the button to be clicked and check for a target’s action to be fired. This means we need to simulate a click on the button. Do we have to use SC.Event for this? No. Instead, we can simulate the event by invoking the button’s mouseDown and mouseUp methods directly. These methods are what do end up being invoked when the button is clicked in the browser. So our test using SproutCore’s unit testing framework might end up being the following:


var view, foo;
module("SC.ButtonView Logic", {
  setup: function() {
    foo = SC.Object.create({
      clicked: NO,
      fireAction: function() {
        this.set('clicked', YES);
      }
    });

    view = SC.ButtonView.create({
      target: foo,
      action: 'fireAction'
    });
  }, 

  teardown: function() {
    view = foo = null;
  }
}); 

test("clicking button will fire action on target 'foo'", function() {
  view.mouseDown();
  view.mouseUp();
  equals(foo.get('clicked'), YES, ' foo.clicked should be YES');
});

The test by itself is pretty basic and we’re obviously not fully exercising the button, but, for the purposes of this discussion, it gets across the idea of simulating an event, a click in this case, by directly invoking methods on a view.

Hold the phone! If we tested the button’s behavior by directly calling methods on it, then why did I bother making you go through understanding what the SC.Event and SC.RootResponder are? For that, we need to look at a slightly more complicated scenario.

A More Complicated Scenario

No matter where you click on the button view, it always reacts in the same way: by firing the same assigned action on the same assigned target. Therefore the button’s clicking behavior is consistent for all intensive purposes. But what if we had a view that reacted differently depending on where the user clicked within the view’s visual boundaries? The idea isn’t so far fetched. In fact, SproutCore’s SC.SegmentedView, SC.RadioView, and SC.ListItemView are three real examples of views that will respond differently depending on where you click inside of them. Let’s use the SC.SegmentedView to understand what I’m talking about.

A segmented view is a view that consists of two or more segments. Each segment has an assigned value and acts like a button. When a segment is clicked it will update the view’s value. You would use a segmented view when you want to allow a user to toggle some behavior or appearance within an application. Basically it’s a radio view but with more features and visualized differently. Let’s make a simple segmented view that will be the following:


toggle: SC.SegmentedView.design({
  layout: { ... },
  items: 'foo bar'.w(),
  value: 'foo'
}),

Rendered in a web browser, you get:

When the user clicks on the ‘foo’ segment, the view’s value will be ‘foo’. And when the user clicks on the ‘bar’ segment, the view’s value will be ‘bar’. Pretty straightforward, right? So, then, how do we simulate clicking on those two segments within the view? Well, it requires that the mouseDown and mouseUp methods are invoked just like we did with the button view, but unlike the button view, we also need supply the methods with an event object that contains the target that was actually clicked on. The target what?

Every action the user performs with a mouse inside of the browser where the HTML is rendered will raise an corresponding mouse event. Each event raised has a target property that points to a specific DOM element that was involved with the event raised. If you mouse down on a DIV element, a corresponding raised event will target that DIV. If you move your mouse over a paragraph element (<P>) then a corresponding raised event will target that element. Knowing this, let’s take a look at how our segmented view is rendered in HTML (with some slight formatting):


<div class="sc-view sc-segmented-view sc-regular-size" id="sc342">

  <a style="display: inline-block;" class="sc-segment sc-first-segment" 
    href="javascript:;">
    <span class="sc-button-inner">
      <label class="sc-button-label">Foo</label>
    </span> 
  </a>

  <a style="display: inline-block;" class="sc-segment sc-last-segment" 
    href="javascript:;">
    <span class="sc-button-inner">
      <label class="sc-button-label">Bar</label>
    </span>
  </a>

</div>

Above, we can see that there is a parent DIV element that acts as the segmented view’s layer. Within the DIV element there are two child anchor (<A>) elements. Those two anchor elements represent each segment within the view. (The main hint is the “sc.segment” class tag). So if the target is an anchor element or any child element of the anchor then that informs the segmented view of what the user tried to click on. Peachy. Now let’s move on to how we simulate clicking on those segments.

To simulate clicking on a segment we need to invoke both the segmented view’s mouseDown and mouseUp methods, supply both methods with an event object, and assign a target to the event. The first option we have at our disposle is to just bypass the moueDown and mouseUp methods entirely and invoke the segmented view’s triggerItemAtIndex method like so:


test("Check that second item is selected correctly, function() {
  view.triggerItemAtIndex(1);
  equals(view.get('value'), 'bar', 'the second item should be selected');          
});

This works, but it’s not exercising the logic within the mouseDown and mouseUp methods, which is what we want to do. Therefore, in order to invoke the two methods, we first need to locate the target element which we can either get directly through the DOM or using the view’s core query object like so:


// Access the target directly through the DOM
target = view.get('layer').childNodes[0];

// or access the target through core query 
target = view.$('.sc-segment').get(0);  

With the target in hand, we could invoke the methods like this:


var view;
module("SC.SegmentedView Logic", {
  setup: function() {
    view = SC.ButtonView.create({
      items: 'foo bar'.w()
    });
    view.createLayer();
  }
}); 

test("Check that second item is selected correctly, function() {
  var target = view.$('.sc-segment').get(1);
  var evt = { target: target };
  view.mouseDown(evt);
  view.mouseUp(evt);
  equals(view.get('value'), 'bar', 'the second item should be selected');          
});

Before our test runs, the module is setup by creating an instance of the segmented button and calling its createLayer method. createLayer will actually cause the view to put together the HTML representing it and convert it into DOM elements. This needs to be done so that you can access the specific DOM element as a target in our test.

With the segmented view now created, we access the specific DOM element via the view’s core query mechanism that will be our target. The target is then assigned to a plain old JavaScript hash object representing the event. Once done, we then supply the event object to both the mouseDown and mouseUp methods and check that the view’s value was updated correctly.

Hmm. Alright. A few things about this updated approach. First, we explicitly had to invoke the view’s createLayer method. Second, we didn’t call the view’s updateLayer method after invoking both mouseUp and mouseDown. This is important in case the view needs to update its visual representation and even internal state. By not calling updateLayer we may have not tested to the view correctly. In addition, calling updateLayer may generate new DOM elements, so we can’t just always assume to use the same DOM element as a target for both mouseDown and mouseUp. Finally, the test is ultimately working outside of the SproutCore framework’s event handling process; therefore we don’t sufficiently simulate the logic in the environment where the view will actually run. As you may have guessed, we can do better.

Actually Simulating an Event

We’ve finally (!) come to the point where, yes, we can now use SC.Event to our advantage. We’ll use SC.Event to trigger an event, and that in turn will invoke an event handler on SC.RootResponder. The root responder, as discussed earlier, will then pass the event along to a pane so that the event cycles up the responder chain and finally make its way to our good ol’ segmented view. Now, the fact that I mentioned “pane” again is important since we’ll need that for our segmented view in order to be part of the responder chain. With all the pieces in place we are now able to properly simulate the actions a user would take to select a segment on a segmented view within the SproutCore’s event responder process. Here’s what we want to do:


var pane, view;
module("SC.SegmentedView Logic", {
  setup: function() {
    SC.RunLoop.begin();
    pane = SC.MainPane.create({
      childViews: [
        SC.SegmentedView.extend({
          layout: { height: 25 },
          items: 'foo bar'.w(), 
        })]
    });
    pane.append();
    SC.RunLoop.end();
    
    view = pane.childViews[0];
  },

  teardown: function() {
    pane.remove();
    pane = view = null;
  } 
});

test("Check that second item is selected correctly, function() {
  var target = view.$('.sc-segment').get(1);
  SC.Event.trigger(target, "mousedown");
  target = view.$('.sc-segment').get(1);
  SC.Event.trigger(target, "mouseup");
  equals(view.get('value'), 'bar', 'the second item should be selected');          
});

You’ll notice now that we’ve removed the explicit calls to the view’s createLayer, mouseDown, and mouseUp. We’ve change our test so that the module is first setup with a main pane that our view becomes a child of. As well, we are now calling SC.Event’s trigger method and passing it a target and the name of the event triggered (the event type). By invoking the trigger method we are essentially kicking off the exact same process that would have occurred if the browser were to raise an event instead. Now we’re cooking with gas!

But Wait! There’s More!

In addition to providing the trigger method with a target element and a event type, you can also supply additional attributes for your event in cases where the view expects more information in order to execute correctly. As an example, you may also want to supply exact client x-y coordinates of where the user actually clicked, or, say, if you wanted to trigger a key up and key down event, you would need to supply the key code, char code, and modifier keys.

When you supply additional attributes to the trigger method, SC.Event will normalize the attributes just like it would if it received an event from the browser, but the event normalization is done implicitly, so you don’t know if your event is being normalized as you would expect it to. To overcome this obstacle, you can use SC.Event’s handy simulateEvent method. The arguments you supply are the same as what you supply to the trigger method, but, instead of kicking of the responder process, you instead get back an event object that has been normalized. You can then pass the event to the trigger method. Here’s a basic example of how to do this:


var target = view.$('.foobar').get(0);
var event = SC.Event.simulateEvent(target, "keydown", {
  which: keyCode || charCode,
  charCode: charCode,
  keyCode: keyCode,
  altKey: isAltKeyDown,
  metaKey: isMetaKeyDown,
  ctrlKey: isControlKeyDown,
  shiftKey: isShiftKeyDown,
});
SC.Event.trigger(target, "keydown", event);

Wrapping Up… For Now

Hopefully this gave you a better understanding of just how events go from the web browser and make it to your view, and how you can perform automated testing of your views with event simulation.

You can also leverage SC.Event and SC.RootResponder beyond test automation, say, when you want to create your own custom events that various parts of your system reacts to, but, for that, I’ll leave it as a future topic to discuss. This should keep ya goin’ for now :-).

Have fun,

-FC

Advertisements