diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..6a67cd4 --- /dev/null +++ b/.jshintrc @@ -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"] +} + diff --git a/bower.json b/bower.json index 25aa663..76ce974 100644 --- a/bower.json +++ b/bower.json @@ -26,6 +26,6 @@ ], "dependencies": { "jQuery": "~2.0.3", - "lodash": "^2.2.1" + "lodash": "^2.4.1" } } diff --git a/src/behave.js b/src/behave.js index a97bbd7..0365e1a 100644 --- a/src/behave.js +++ b/src/behave.js @@ -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); @@ -12,7 +22,7 @@ Behave.domTypes = { attrOptions: ['name', 'for', 'placeholder', 'contains', 'type', 'test-me'] }, clickable: { - elementTypes: ['button', 'a'], + elementTypes: ['button', 'a', 'select'], attrOptions: ['contains', 'href', 'test-me'] }, icon: { @@ -20,34 +30,32 @@ Behave.domTypes = { 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) { @@ -55,7 +63,7 @@ Behave.find = function(identifier, type) { } }); }); - }) + }); if (element && element.is('label')) { element = getClosestInput(element); @@ -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)) { @@ -96,43 +124,94 @@ 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;} @@ -140,7 +219,7 @@ var cleanVal = function(val) { // 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); @@ -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; diff --git a/test/templates.js b/test/templates.js index f52a5bb..774b3a9 100644 --- a/test/templates.js +++ b/test/templates.js @@ -57,4 +57,87 @@ " \n" + " \n" + " \n" + - "" + ""; + + templates.invoiceOne = + "
\n" + + " {{data.invoice.amount_in_cents | fromCents | currency }}\n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "\n" + + " {{data.invoice.invoice_date | date}}\n" + + "
\n" + + "