Skip to content

Commit

Permalink
Merge pull request #8 from hinthealth/feature/betterMatching
Browse files Browse the repository at this point in the history
Feature/better matching and other improvements
  • Loading branch information
blakewest committed Oct 18, 2014
2 parents e943f68 + ba34f54 commit 5abcf59
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 87 deletions.
87 changes: 87 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
// JSHint Configuration File for Testing
// See http://jshint.com/docs/ for more details

"maxerr" : 50, // {int} Maximum error before stopping

// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : false, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : true, // true: Require variables/functions to be defined before being used
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : "false", // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
"globalstrict" : true, // true: Allow global "use strict" (also enables 'strict')
"trailing" : true, // true: Prohibit trailing whitespaces
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : 120, // {int} Max number of characters per line

// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements"
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function

// Environments
"browser" : true, // Web Browser (window, document, etc)
"couch" : false, // CouchDB
"devel" : false, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : true, // jQuery
"mootools" : false, // MooTools
"node" : false, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface

// Legacy
"nomen" : false, // true: Prohibit dangling `_` in variables
"onevar" : false, // true: Allow only one `var` statement per function
"passfail" : false, // true: Stop on first error
"white" : true, // true: Check against strict whitespace and indentation rules

// Custom Globals
"predef" : [ "_" , "$", "angular", "moment", "Stripe", "analytics", "bowser", "Behave"]
}

2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
],
"dependencies": {
"jQuery": "~2.0.3",
"lodash": "^2.2.1"
"lodash": "^2.4.1"
}
}
174 changes: 128 additions & 46 deletions src/behave.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
// Little jQuery extension to have exact equals;
$.expr[':'].textEquals = function(a, i, m) {
var match = $(a).text().match("^" + m[3] + "$")
'use strict';
// Little jQuery extension for exact and rough text matching
$.expr[':'].textEquals = function(el, i, m) {
var textToMatch = m[3];
var match = $(el).text().trim().match("^" + textToMatch + "$")
return match && match.length > 0;
}
};

// This is similar to contains, except not case sensitive.
$.expr[':'].textRoughMatch = function(el, i, m) {
var textToMatch = m[3];
var elText = $(el).text().toLowerCase();
var res = elText.toLowerCase().indexOf(textToMatch.toLowerCase()) !== -1;
return res;
};

window.Behave = {};
Behave.view = $(document.body);
Expand All @@ -12,50 +22,48 @@ Behave.domTypes = {
attrOptions: ['name', 'for', 'placeholder', 'contains', 'type', 'test-me']
},
clickable: {
elementTypes: ['button', 'a'],
elementTypes: ['button', 'a', 'select'],
attrOptions: ['contains', 'href', 'test-me']
},
icon: {
elementTypes: ['icon', 'div', 'span'],
attrOptions: ['type', 'class', 'test-me']
},
display: {
elementTypes: [''], // This is actually all elements, since there's no leading el type
// The empty string is actually all elements, since there's no leading el type
elementTypes: ['table', 'tr', 'td', 'th', ''],
attrOptions: ['test-me', 'contains']
}
}

Behave.getAllElsAttrOptions = ['name', 'for', 'placeholder', 'type', 'test-me']
};

Behave.find = function(identifier, type) {
Behave.find = function(identifier, type, opts) {
opts = opts || {};
var element = '';
var searchOptions = type ? {specificOption: Behave.domTypes[type]} : Behave.domTypes;
_.each(searchOptions, function(searchParams) {
_.each(searchParams.elementTypes, function(elType) {
if (element.length) {
// Explicitly returning false kills iteration early in lodash.
return false;
}
// Explicitly returning false kills iteration early in lodash.
if (element.length) {return false;}

_.each(searchParams.attrOptions, function(attrOption) {
switch (attrOption) {
case 'contains':
var filter = ":textEquals("+ identifier + ")"
element = Behave.view.find(elType + filter);
element = findByText(identifier, elType);
break;
case 'class':
element = findByClass(identifier, elType, 'glyphicon-');
element = findByClass(identifier, elType);
break;
default:
var filter = "[" + attrOption + "='" + identifier + "']";
element = Behave.view.find(elType + filter);
element = tryToFind(elType + filter);
}
// Explicitly returning false kills iteration early in lodash.
if (element.length) {
return false;
}
});
});
})
});

if (element && element.is('label')) {
element = getClosestInput(element);
Expand All @@ -64,14 +72,34 @@ Behave.find = function(identifier, type) {
if (!element.length) {
element = Behave.view.find(identifier);
}

// No element has been found, and we're in error mode.
if (!element.length && !opts.noErrors) {
throw new Error('Can\'t find element identified by ' + identifier);
}

// We found too many things
if (element.length > 1 && !opts.findMany) {
throw new Error('Matched multiple elements identified by ' + identifier + '. Use findAll if that\'s what you expect.')
}

return element;
};

Behave.tryFind = function(identifier, type) {
return Behave.find(identifier, type, {noErrors: true});
};

Behave.findAll = function(identifier, type) {
return Behave.find(identifier, type, {findMany: true});
};


Behave.fill = function(identifier) {
// If id is already jQuery, just go with it. Useful if you've set a variable using Behave.find, and then want to
// reuse that variable in a fill later on.
var $el = identifier instanceof jQuery ? identifier : Behave.find(identifier, 'field');

var fillWith = function(data) {
if ($el.is('form') || $el.attr('type') === 'form') {
if (!_.isObject(data)) {
Expand All @@ -96,51 +124,102 @@ Behave.fill = function(identifier) {
return fillObject;
};

Behave.getAllEls = function(element, $els) {
element = element || Behave.view;
$els = $els || {};
var kids = element.children;
if (kids.length) {
element.children().each(function() {
$els = Behave.getAllEls($(this), $els);
});
Behave.bexpect = function(identifier, opts) {
if (_.isObject(jasmine)) {
if (_.isString(identifier)) {
return jasmine.getGlobal().expect(Behave.find(identifier, opts));
}
return jasmine.getGlobal().expect(identifier);
}
_.each(Behave.getAllElsAttrOptions, function(attrOption) {
var attrVal = cleanVal(element.attr(attrOption));
if (attrVal) {
element.reload = function() {
return Behave.find(attrVal);
}

throw new Error("It appears jasmine or expect isn't defined. Thus Behave can't delegate expect");
};

Behave.click = function(identifier) {
if (identifier instanceof jQuery) {
if (identifier.is('input') && identifier.attr('type') === 'radio') {
return identifier.click().trigger('click');
} else {
return identifier.trigger('click');
}
attrVal && ($els[attrVal] = element)
});
var elText = element.text();
if(elText) {$els[cleanVal(elText)] = element;}
return $els;
return;
}
if (_.isString(identifier)) {
return Behave.find(identifier).trigger('click');
}
throw new Error("The identifier passed to click was invalid. It must be either a string or jQuery object");
};

Behave.choose = function(value) {
// If id is already jQuery, just go with it. Useful if you've set a variable using Behave.find, and then want to
// reuse that variable in a fill later on.

var chooseFrom = function(dropDown) {
var $el = dropDown instanceof jQuery ? dropDown : Behave.find(dropDown);
return $el.val(value).trigger('change');
};

var chooseObject = {
from: chooseFrom
};

return chooseObject;
};


// PRIVATE FUNCTIONS
var tryToFind = function(expression) {
var el = '';
try {
el = Behave.view.find(expression);
}
catch (e) {
// Syntax errors occur sometimes when trying to do certain operations
// with ~'s and such. We just want it to return nothing in this case.
if ( !(_.contains(e.message, "Syntax error")) ) {
// Re throw if it's not a syntax errors
throw (e)
}
}
return $(el);
};

var getClosestInput = function($el) {
var sibling = $el.next();
if (sibling.is('input')) {return sibling}
if (sibling.is('input')) {return sibling;}
var relatedInput = sibling.find('input');
return relatedInput.length ? relatedInput : $el;
};

var findByClass = function(identifier, elType, prefix) {
prefix = prefix || '';
var findByClass = function(identifier, elType) {
var prefix = _.contains(['icon', 'div', 'span'], elType) ? 'glyphicon-' : '';
elType = elType || '';
return Behave.view.find(elType + '.' + prefix + identifier).first();
}
var expression = elType + '.' + prefix + identifier;

return tryToFind(expression).first();
};

var findByText = function(identifier, elType) {
var filterMethod, filterString, expression;
filterMethod = ":textEquals";

if (identifier[0] === '~') {
identifier = identifier.slice(1);
filterMethod = ":textRoughMatch";
}
filterString = filterMethod + "('" + identifier + "')";
expression = elType + filterString;

return tryToFind(expression);
};

var cleanVal = function(val) {
if (!val) {return;}

// Remove any spaces.
val = val.replace(' ', '');

if (val.indexOf('-') !== -1) {
if (_.contains(val, '-')) {
// camelCasing attrs with dashes in them.
var words = val.split('-');
words[1] = words[1][0].toUpperCase() + words[1].substring(1);
Expand All @@ -152,5 +231,8 @@ var cleanVal = function(val) {

// Set functions to the window for convenience
window.find = Behave.find;
window.fill = Behave.fill

window.fill = Behave.fill;
window.findAll = Behave.findAll;
window.tryFind = Behave.tryFind;
window.bexpect = Behave.bexpect;
window.click = Behave.click;
Loading

0 comments on commit 5abcf59

Please sign in to comment.