Unit testing components in Appcelerator’s Titanium Alloy

Using the code for creating a pull-to-refresh headerView from one of Appcelerator’s tutorial, the next challenge is trying to break it down into a testable units. We can do this by setting up tijasmine in our project and using jasmine spies. I followed the steps at in another post to setup my project so please follow this link. After doing that create a new file called pullToRefresh_spec.js in the lib folder at app/lib/specs/lib with the following code


require("/tijasmine/tijasmine").infect(this);

describe("pullToRefresh", function () {

var pullToRefresh = require('pullToRefresh')
 , table// = Ti.UI.createTableView()
 , pullText = Alloy.CFG.pull_to_refresh_text
 , releaseText = Alloy.CFG.release_to_refresh_text
 , callback// = jasmine.createSpy()
 , pullingEvent// =
 , pulledEvent// =
 , draggedEvent
 , options

beforeEach(function () {
 table = Ti.UI.createTableView()
 options = {
 onPulling: jasmine.createSpy()
 , onPulled: jasmine.createSpy()
 , onDragEnd: jasmine.createSpy()
 , doAnimation: 'false'
 }

pullToRefresh.setup(table, options)

pullingEvent = {
 _offset: -1, _pulling: true, _reloading: false, _testing: true
 }
 pulledEvent = {
 _offset: -90, _pulling: false, _reloading: false, _testing: true
 }
 draggedEvent = {
 _pulling: true, _reloading: false, _offset: -90, _testing: true
 }
 })

 it('should add a headerPullView to the table ', function () {
 expect(table.getHeaderPullView()).toBeDefined()
 })

 it('should have a label that says, ' + pullText, function () {
 expect(pullToRefresh.message.getText()).toBe(pullText)
 })

it('should say, ' + releaseText + ', and callback with "pulled" when it is pulled',
 function () {
 runs(function (){ table.fireEvent('scroll', pulledEvent) })

waitsFor(function () {
 return options.onPulled.callCount > 0
 }, "fireEvent timed out", 9000)

runs(function () {
 expect(options.onPulled).toHaveBeenCalledWith('pulled')
 expect(pullToRefresh.message.getText()).toBe(releaseText)
 })
 })

it('should say, ' + pullText + ', and callback with "pulling" when it is pulling',
 function () {
 runs(function (){ table.fireEvent('scroll', pullingEvent) })

waitsFor(function () {
 return options.onPulling.callCount > 0
 }, "fireEvent timed out", 9000)

runs(function () {
 expect(options.onPulling).toHaveBeenCalledWith('pulling')
 expect(pullToRefresh.message.getText()).toBe(pullText)
 })
 })

it('should update the timestamp when pulled', function () {
 runs(function (){ table.fireEvent('scroll', pulledEvent) })

waitsFor(function () {
 return options.onPulled.callCount > 0
 }, "fireEvent timed out", 9000)

runs(function () {
 expect(pullToRefresh.timestamp.getText()).
 toMatch(/(Last updated: )(0?[1-9]|[12][0-9]|3[01])[- \/.](0?[1-9]|1[012])[- \/.](19|20)\d\d/)
 })
 })

it('should say, Updating..., when it is dragged and callback with "dragEnd" and it should say, ' + pullText + ', and timestamp when it is reset',
 function () {
 runs(function (){ table.fireEvent('dragEnd', draggedEvent) })

waitsFor(function () {
 return options.onDragEnd.callCount > 0
 }, "fireEvent timed out", 9000)

runs(function () {
 expect(pullToRefresh.message.getText()).toBe('Updating...')
 expect(options.onDragEnd).toHaveBeenCalledWith('dragEnd')

pullToRefresh.reset()
 // ..then reset
 expect(pullToRefresh.message.getText()).toBe(pullText)
 expect(pullToRefresh.timestamp.getText()).
 toMatch(/(Last updated: )(0?[1-9]|[12][0-9]|3[01])[- \/.](0?[1-9]|1[012])[- \/.](19|20)\d\d/)

})
 })
})

Make sure to add a reference to this spec in the test_runner.js file. These are the tests which should all be failing because we don’t have any implementation of this component. That can be fixed by adding pullToRefresh.js to app/lib


// enables pull-to-refresh
// attaches to a table and adds a header that
// shows an animated arrow, an activity indicator and
//
var dates = require('dates')
 , pullText = Alloy.CFG.pull_to_refresh_text
 , releaseText = Alloy.CFG.release_to_refresh_text
 , lastUpdatedText = Alloy.CFG.last_updated_text
 , pulling = false
 , reloading = false
 , offset = 0
 , tableHeader = Ti.UI.createView({
 backgroundColor:'#e2e7ed',
 width:320, height:60
 })
 , border = Ti.UI.createView({
 backgroundColor:'#576c89',
 bottom:0,
 height:2
 })
 , imageArrow = Ti.UI.createImageView({
 image:'/images/whiteArrow.png',
 left:20, bottom:10,
 width:23, height:60
 })
 , labelStatus = Ti.UI.createLabel({
 color:'#576c89',
 font:{fontSize:13, fontWeight:'bold'},
 text:'Pull down to refresh...',
 textAlign:'center',
 left:55, bottom:30,
 width:200
 })
 , labelLastUpdated = Ti.UI.createLabel({
 color:'#576c89',
 font:{fontSize:12},
 text: lastUpdatedText + dates.getFormattedDate(),
 textAlign:'center',
 left:55, bottom:15,
 width:200
 })
 , actInd = Ti.UI.createActivityIndicator({
 left:20, bottom:13,
 width:30, height:30
 })
 , tableRowTotal = 0
 , _table
 // in the simulator tests sometimes break when the view
 // is not rendered before the test runs so allow a flag
 // to prevent animation calls
 , _doAnimation = 'true'
 , shouldDoAnimation = function (){
 return _doAnimation === 'true'
 }
// bind events to table and set optional parameters
exports.setup = function (table, options) {

_table = table
 options = options || {}

_doAnimation = options.doAnimation || _doAnimation

 table.headerPullView = tableHeader

table.addEventListener('dragEnd', function (e) {

if (pulling && !reloading && offset < -80) {
 onDragEnd(e, options.onDragEnd)
 }
 })

// when the user pulls down or releases the table update the labels
 table.addEventListener('scroll', function (e) {

 // so we can test let us be able to stub the values
 offset = e._offset || e.contentOffset.y
 pulling = e._pulling || pulling
 reloading = e._reloading || reloading

// pulling
 if (pulling && !reloading && offset > -80 && offset < 0) {

onPulling(options.onPulling)
 // not pulling/pulled
 } else if (!pulling && !reloading && offset < -80) {

onPulled(options.onPulled)
 }
 })
}

// show timestamp on pull header and reset message
function reset (table) {

table = table || _table

reloading = false
 labelLastUpdated.text = lastUpdatedText + getFormattedDate();
 actInd.hide();
 imageArrow.transform = Ti.UI.create2DMatrix();
 imageArrow.show();
 labelStatus.text = pullText
 _table.setContentInsets({top:0}, {animated: shouldDoAnimation()})
}

// update the label to Updating...
// and execute callback
function onDragEnd (e, callback) {

 pulling = false;
 reloading = true;
 labelStatus.text = 'Updating...';
 imageArrow.hide();
 actInd.show();
 if(e && e.source) {
 _table = e.source
 // leave margin at the top to show Updating... message
 e.source.setContentInsets({top:80}, {animated: shouldDoAnimation()});
 //setTimeout(function(){
 // loadTableData(e.source, 5, resetPullHeader(e.source));
 //}, 2000);
 }

if(typeof callback == 'function'){
 Ti.API.debug('pullToRefresh: calling back with dragEnd')

callback('dragEnd')
 }
}

// set message to pullText
function onPulling (callback) {
 Ti.API.debug('pullToRefresh : ' + pullText)

pulling = false
 labelStatus.text = pullText

 if(typeof callback == 'function'){
 Ti.API.debug('pullToRefresh: calling back with pulling')

callback('pulling')
 }

 if(shouldDoAnimation()){
 var unrotate = Ti.UI.create2DMatrix()
 imageArrow.animate({ transform:unrotate, duration:180 })
 }
}

// set message to releaseText
function onPulled (callback) {

Ti.API.debug('pullToRefresh : ' + releaseText)

pulling = true
 labelStatus.text = releaseText

if(typeof callback == 'function'){
 Ti.API.debug('pullToRefresh: calling back with pulled')

callback('pulled')
 }

 if(shouldDoAnimation()){
 var rotate = Ti.UI.create2DMatrix().rotate(180)
 imageArrow.animate({ transform:rotate, duration:180 })
 }
}

tableHeader.add(border)
tableHeader.add(imageArrow)
tableHeader.add(labelStatus)
tableHeader.add(labelLastUpdated)
tableHeader.add(actInd)

// export ui items for testing
exports.message = labelStatus
exports.timestamp = labelLastUpdated
exports.busy = actInd
exports.reset = reset

I would have preferred to use events instead of callbacks to decouple the features of the pullToRefresh component but I had problems getting the jasmine tests to pass as the “spyOn” feature didn’t seem to work as I hoped. Anyway, as it is, we can pass any table to this component and it will add some label feedback to the user and allow us to perform an action as the table is pulled like this –


// enable pull-to-refresh

var pullToRefresh = require('pullToRefresh')

pullToRefresh.setup($.tableView, {
onDragEnd: function () {

doSomethingWithAjax({ success: function () {

pullToRefresh.reset()
}
})
 }
})

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s