Skip to content

Instantly share code, notes, and snippets.

@mattlanham
Created February 11, 2011 23:51
Show Gist options
  • Save mattlanham/823294 to your computer and use it in GitHub Desktop.
Save mattlanham/823294 to your computer and use it in GitHub Desktop.
/*
Copyright 2010 Kevin Whinnery
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var he = {};
/*
Helium Test
-----------
super simple unit testing framework
*/
(function() {
he.test = {}; //test namespace
var unitTests = [],
failureMessages = [],
testsExecuted = 0,
assertions = 0,
successes = 0,
failures = 0,
warnings = 0,
executing = false;
/*
Add a test to the unit test suite. Possible arguments are:
{
name: String, name of unit test
suite: String (optional), name of unit test suite this test belongs to
setup: Function (optional), perform any setup that needs to happen prior to the test being run
//and one of...
visual: {
build: Function, create a view, to be added to a view inside a window, to create a visual test
conditions: String[], a description of the conditions that need to be true for the test to pass
}
//or...
unit: Function, a non-visual test to be run - must call he.test.done to complete, as all
tests are assumed to be asynchronous
}
*/
he.test.add = function(/*Object*/ test) {
unitTests.push(test);
};
//Flip a boolean flag to indicate a test has completed running
he.test.done = function() {executing=false;};
//Insert a warning
he.test.warn = function(/*String*/ description) {
warnings++;
Ti.API.warn('[Helium] UNIT TEST WARNING: '+description);
};
//Assert that a given condition is true
he.test.assert = function(/*Boolean*/ value, /*String*/ description) {
assertions++;
if (value) {
successes++;
Ti.API.debug('[Helium] UNIT TEST PASSED: '+description);
}
else {
failures++;
failureMessages.push(description);
Ti.API.error('[Helium] UNIT TEST FAILED: '+description);
}
};
//Run all tests, or the given suite
he.test.run = function(/*String [optional]*/ suite) {
//reset and clone unit test set for a fresh run
var tests = unitTests.slice();
failureMessages = [];
assertions = 0;
successes = 0;
warnings = 0;
failures = 0;
//current set of visual test conditions
var visualConditions = [];
//Set up UI tests
var userTestView = null, testWindow = null; //will be overridden and re-added with every visual test
var testControls = Ti.UI.createView({
height:100,
bottom:5,
left:5,
right:5
});
function continueVisual() {
visualConditions.shift();
if (visualConditions.length > 0) {
conditionText.text = visualConditions[0];
}
else {
conditionText.text = 'Waiting...';
he.test.done();
testWindow.close();
}
}
var noButton = Ti.UI.createButton({
title:'No',
right:0,
width:50,
height:50
});
noButton.addEventListener('click', function() {
he.test.assert(false,visualConditions[0]);
continueVisual();
});
var yesButton = Ti.UI.createButton({
title:'Yes',
right:55,
width:50,
height:50
});
yesButton.addEventListener('click', function() {
he.test.assert(true,visualConditions[0]);
continueVisual();
});
var conditionText = Ti.UI.createLabel({
text:'Waiting...',
color:'#000',
font:{
fontSize:12
},
top:0,
left:0,
bottom:0,
right:110
});
testControls.add(yesButton);
testControls.add(noButton);
testControls.add(conditionText);
var testViewContainer = Ti.UI.createView({
backgroundColor:'#fff',
bottom:110,
left:5,
right:5,
top:5
});
Ti.API.info('********* Begin Unit Tests ***********');
Ti.API.info('Begin hardcore testing action...');
var timer = setInterval(function() {
if (tests.length == 0 && !executing) {
clearInterval(timer);
Ti.API.info('Test run finished! So, how did we do?');
Ti.API.info('Total assertions: ' + assertions);
Ti.API.info('Successes: ' + successes);
Ti.API.info('Warnings: ' + warnings);
Ti.API.info('Failures: ' + failures);
//alert uses setTimeout to get around android timing issues
var title = 'Tests Complete';
var msg = 'Yeah, baby! No test failures!';
if (failures > 0) {
Ti.API.error('Aww, hamburgers! You have '+failures+' failed tests - check the log for details.');
msg = 'D\'oh! The following tests failed: \n';
for (var j=0;j<failureMessages.length;j++) {
msg=msg+failureMessages[j]+'\n';
}
}
var alertDialog = Titanium.UI.createAlertDialog({
title: 'Tests Complete',
message: msg,
buttonNames: ['OK']
});
alertDialog.show();
Ti.API.info('********* End Unit Tests ***********');
}
if (!executing && tests.length > 0) {
executing = true;
var tst = tests.shift();
if (!suite || tst.suite == suite) {
Ti.API.debug('[Helium] Executing Test: '+tst.name);
if (tst.setup != undefined) {
tst.setup.call(tst);
}
if (tst.unit != undefined) {
tst.unit.call(tst);
if (!tst.asynch) {executing = false;}
testsExecuted++;
}
else { //assuming visual test in this case
if (userTestView != null) {
testViewContainer.remove(userTestView);
}
visualConditions = tst.visual.conditions;
userTestView = tst.visual.build();
testViewContainer.add(userTestView);
conditionText.text = visualConditions[0];
//grr... window management cruft for Android - need to re-create every time
testWindow = Ti.UI.createWindow({
backgroundColor:'#787878'
});
testWindow.add(testControls);
testWindow.add(testViewContainer);
testWindow.open({animated:false});
}
}
else {
executing = false;
}
}
},50);
};
})();
/*
Helium Core
-----------
Core utilities and helper functions.
*/
(function() {
he.isArray = function(obj) {
return !!(obj && obj.concat && obj.unshift && !obj.callee);
};
//Extend an object with the properties from another (thanks Dojo)
//public API defined by he.extend - calls private function mixin
var empty = {};
function mixin(/*Object*/ target, /*Object*/ source){
var name, s, i;
for(name in source){
if (source.hasOwnProperty(name)) {
s = source[name];
if(!(name in target) || (target[name] !== s && (!(name in empty) || empty[name] !== s))){
target[name] = s;
}
}
}
return target; // Object
};
he.extend = function(/*Object*/ obj, /*Object...*/ props){
if(!obj){ obj = {}; }
for(var i=1, l=arguments.length; i<l; i++){
mixin(obj, arguments[i]);
}
return obj; // Object
};
//Ensure scripts are Ti-included exactly once in the current context
var scriptRegistry = {};
he.load = function(/*String... arguments*/) {
for (var i=0;i<arguments.length;i++) {
if (scriptRegistry[arguments[i]]==undefined) {
scriptRegistry[arguments[i]]=0;//loaded
Ti.include(arguments[i]);
}
}
};
//Flexible wrappers around the properties API for persisting any JavaScript object
he.hasProperty = function(/*String*/ key) {
return Ti.App.Properties.hasProperty(key);
};
he.saveObject = function(/*String*/ key,/*Object*/ val) {
Ti.App.Properties.setString(key,JSON.stringify(val));
};
he.loadObject = function(/*String*/ key) {
var value = Ti.App.Properties.getString(key);
try {
return JSON.parse(value);
} catch (e) {
return value;
}
};
//Shorthand for app-level events
var listeners = [];
he.pub = function(/*String*/ name,/*Object*/ data) {
//first, check to see if we have a regex listener and call it if we do
for (var i = 0;i < listeners.length;i++) {
var listener = listeners[i];
if (listener.regex.test(name)) {
listener.fn.call(this,data);
}
}
//then, pass through to standard Titanium event handling
Ti.App.fireEvent(name,data||{});
};
he.sub = function(/*String or RegExp*/ nameOrRegex,/*Function*/ fn) {
//string indicated plain old subscribe
if (typeof(nameOrRegex) == 'string') {
Ti.App.addEventListener(nameOrRegex,fn);
}
else {
//assume we have a regular expression and add it to listeners
listeners.push({
regex:nameOrRegex,
fn:fn
});
}
};
//OS, Locale, and Density specific branching helpers
var density = Ti.Platform.displayCaps.density;
var locale = Ti.Platform.locale;
var osname = Ti.Platform.osname;
he.density = function(/*Object*/ map) {
var type;
if (density == 'high') {
type = typeof(map.high);
if (type != 'undefined') {
if (type == 'function') { return map.high(); }
else { return map.high; }
}
}
else if (density == 'medium') {
type = typeof(map.medium);
if (type != 'undefined') {
if (type == 'function') { return map.medium(); }
else { return map.medium; }
}
}
else if (density == 'low') {
type = typeof(map.low);
if (type != 'undefined') {
if (type == 'function') { return map.low(); }
else { return map.low; }
}
}
};
/*
iPhone positioning is based on a point system, which is always 320x480.
Android has many, many different screen densities and we don't support a
point system. For cross-platform positioning, we need to normalize for this
by assuming the 'normal' value is the point value for iPhone, and the 320x480
pixel value for Android. You can then specify a android high density pixel value,
then an Android low density pixel value.
This sucks - hopefully Helium or Titanium can get smarter about this.
*/
he.pt = function(/*Number*/ normal,/*Number*/ androidHigh,/*Number*/ androidLow) {
if (osname == 'android') {
if (density == 'high' && androidHigh) {return androidHigh;}
if (density == 'low' && androidLow) {return androidLow;}
}
return normal;
};
he.locale = function(/*Object*/ map) {
var def = map.def||null; //default function or value
if (map[locale]) {
if (typeof map[locale] == 'function') { return map[locale](); }
else { return map[locale]; }
}
else {
if (typeof def == 'function') { return def(); }
else { return def; }
}
};
he.os = function(/*Object*/ map) {
var def = map.def||null; //default function or value
if (map[osname]) {
if (typeof map[osname] == 'function') { return map[osname](); }
else { return map[osname]; }
}
else {
if (typeof def == 'function') { return def(); }
else { return def; }
}
};
})();
/*
Helium Network
--------------
Ajax and remote server communication helpers
*/
(function() {
/*
Simplified Ajax API for remote services
Parameters:
url: String, The resource you are addressing via HTTP
options: {
method: String ('GET'), The HTTP method you're using
body: Object or XML/JSON String - JavaScript object with parameters (usually translated to form-encoded for POST)
headers: Array and array of key/value objects for the HTTP headers for this request
format: String, one of ['xml','json','text'] - determines what your handlers get back as 1st arg
success: function(responseData,HTTPClient) - success handler is fired onload - get your specified data type and raw XHR
error: function(HTTPClient) - called on any error condition - get raw XHR
}
*/
//he.ajax = function(/*String*/ url, /*Object*/ options) {
//TODO: Need to give this API more thought
//};
})();
/*
Helium UI
---------
Helpers for registering and styling Titanium and app-specific components
*/
(function() {
var psets={};
//Register new psets - currently all psets are global (like CSS)
he.registerPsets = function(/*Object*/ propertySets) {
he.extend(psets,propertySets);
};
//get the pset by the given key
he.getPset = function(/*String*/ key) {
return he.extend({},psets[key]); //return clone
};
//apply a pset to a selected target
he.applyPset = function(/*Object*/ target /*String... pset names*/) {
for (var j = 1; j < arguments.length; j++) {
he.extend(target,psets[arguments[j]]);
}
return target;
};
//transition pset properties
he.transform = function(/*Object*/ target, /*String or Array*/ pset, /*Number [optional]*/ duration, /*Function [optional]*/ callback) {
var args = {};
if (typeof(pset) == 'string') {
args = he.getPset(pset);
}
else {
for (var i=0;i<pset.length;i++) {
he.applyPset(args,pset[i]);
}
}
/*
he.os({
iphone:function() {args.transform = Ti.UI.create2DMatrix();}
});
*/
args.duration=duration||0;
target.animate(args,callback||function(){});
};
/*
Register a custom UI builder, which will be passed merged pset arguments
*/
var widgetRegistry = {};
he.register = function(/*String*/ widgetName, /*Function*/ builder) {
var widget = {};
widget[widgetName] = builder;
he.extend(widgetRegistry,widget);
};
/*
Create a Titanium or custom UI component, styled with psets
*/
he.create = function(/*String*/ viewType /* String or Object... psets or plain objects */) {
var args = {};
//First, grab widget defaults if we have them
he.applyPset(args,viewType);
for (var j = 1; j < arguments.length; j++) {
if (typeof(arguments[j]) == 'string') {
he.applyPset(args,arguments[j]);
}
else {
he.extend(args,arguments[j]);
}
}
//Use a custom builder if we have it, otherwise assume it's a Ti.UI namespaced object
if (widgetRegistry.hasOwnProperty(viewType)) {
return widgetRegistry[viewType](args);
}
else {
try {
return Ti.UI['create'+viewType](args);
} catch(e) {
Ti.API.error('[Helium.create] No constructor found for type: '+viewType);
return null;
}
}
};
})();
// Titanium doesn't like it if you done use their standard way of creating objects,
// this keeps Titanium nice and happy
Ti.UI.create2DMatrix();
Ti.UI.create3DMatrix();
Ti.UI.createActivityIndicator();
Ti.UI.createAlertDialog();
Ti.UI.createAnimation();
Ti.UI.createButton();
Ti.UI.createButtonBar();
Ti.UI.createCoverFlowView();
Ti.UI.createDashboardItem();
Ti.UI.createDashboardView();
Ti.UI.createEmailDialog();
Ti.UI.createImageView();
Ti.UI.createLabel();
Ti.UI.createOptionDialog();
Ti.UI.createPicker();
Ti.UI.createPickerColumn();
Ti.UI.createPickerRow();
Ti.UI.createProgressBar();
Ti.UI.createScrollView();
Ti.UI.createScrollableView();
Ti.UI.createSearchBar();
Ti.UI.createSlider();
Ti.UI.createSwitch();
Ti.UI.createTab();
Ti.UI.createTabGroup();
Ti.UI.createTabbedBar();
Ti.UI.createTableView();
Ti.UI.createTableViewRow();
Ti.UI.createTableViewSection();
Ti.UI.createTextArea();
Ti.UI.createTextField();
Ti.UI.createToolbar();
Ti.UI.createView();
Ti.UI.createWebView();
Ti.UI.createWindow();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment