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() } }) } })