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" + + "
\n" + + "

Invoice # {{data.invoice.invoice_number}}

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "

\n" + + " {{data.invoice.amount_in_cents | fromCents | currency }}\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "

\n" + + "

\n" + + "  \n" + + " \n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "

\n" + + " {{data.invoice.invoice_date | date}}\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
"; + + templates.dropdowns = + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n"; + diff --git a/test/test.js b/test/test.js index 113d478..69f9ea4 100644 --- a/test/test.js +++ b/test/test.js @@ -1,16 +1,24 @@ /* jshint immed: false */ -/* globals Replicator, describe, beforeEach, it, xdescribe, templates */ +/* globals Replicator, describe, beforeEach, it, xdescribe, templates, Behave */ +/* quotmark: true*/ (function () { 'use strict'; var $view; beforeEach(function() { - Behave.view = $view = $(templates.signup) + Behave.view = $view = $(templates.signup); }); describe('#find', function() { + describe("when no element is found", function() { + it("should throw an error", function() { + (function() { + var el = Behave.find("TOTALLY NOT HERE") + }).should.throw("Can't find element identified by TOTALLY NOT HERE"); + }); + }); describe("with field type elements", function() { it("should, by default, look for field type elements", function() { - $view.append('div[name=subdomain]') + $view.append('div[name=subdomain]'); var jqSubdomain = $view.find("input[name='subdomain']"); var bSubdomain = Behave.find('subdomain'); bSubdomain.attr('ng-model').should.eql(jqSubdomain.attr('ng-model')); @@ -40,12 +48,63 @@ bSubdomain.attr('ng-model').should.eql(correspondingInput.attr('ng-model')); }); }); + + describe("when multiple things match", function() { + beforeEach(function() { + Behave.view.append("
Worked!
") + Behave.view.append("
WorkedAgain!
") + }); + it("should throw an error if multiple things match", function() { + (function() { + Behave.find('~Worked') + }).should.throw('Matched multiple elements identified by ~Worked. Use findAll if that\'s what you expect.'); + }); + describe("using find all", function() { + it("should be totally fine", function() { + (function() { + Behave.findAll('~Worked'); + }).should.not.throw(); + }); + it("should return the correct number of elements", function() { + Behave.findAll('~Worked').length.should.eql(2); + }); + }); + }); + + describe("searching by text", function() { + describe("with exact matching", function() { + beforeEach(function() { + Behave.view = $view = $(templates.invoiceOne); + }); + it("should only find things with the exact text", function() { + var jqResult = $view.find("a:contains('Back to All Invoices')"); + var bResultNotExact = Behave.tryFind("Back to All"); + var bResultExact = Behave.find("Back to All Invoices"); + bResultExact.text().should == "Back to All Invoices"; + bResultExact.text().should.eql(jqResult.text()); + bResultNotExact.text().should.eql(''); + }); + }); + describe("when using rough match", function() { + beforeEach(function() { + Behave.view.append("
Success: This is alert text that could be many things!
") + }); + it("should find a substring match", function() { + var bResult = Behave.find('~Success'); + bResult.text().should.eql("Success: This is alert text that could be many things!"); + }); + it("should not be case sensitive", function() { + var bResult = Behave.find('~sUcCess'); + bResult.text().should.eql('Success: This is alert text that could be many things!'); + }); + }); + }); describe("with clickable type elements", function() { var jqResult, bResult; beforeEach(function() { - $view.append(''); + $view.append(""); $view.append("Practice Url"); - jqResult = $view.find('button:contains(Subdomain)') + jqResult = $view.find('button:contains(Subdomain)'); }) it("should find only clickable type elements and search by containing text", function() { bResult = Behave.find('Subdomain', 'clickable'); @@ -53,13 +112,6 @@ bResult.is('button').should.eql(true); bResult.text().should.eql(jqResult.text()); }); - it("should default to doing an exact search", function() { - var bRoughResult = Behave.find('Subdo', 'clickable'); - var bExactResult = Behave.find('Subdomain', 'clickable'); - bRoughResult.is('button').should.eql(false); - bExactResult.is('button').should.eql(true); - bExactResult.text().should.eql(jqResult.text()); - }); it("should find things based on href", function() { bResult = Behave.find("www.test.com", 'clickable'); jqResult = $view.find("a[href='www.test.com']"); @@ -71,11 +123,6 @@ bResult.text().should.eql(jqResult.text()); }); }); - describe("with display type elements", function() { - xit("should return rough matches of text contained in display type elements", function() { - // TODO: Create good list of display elements, or maybe just use body:contains ? figure this out. - }); - }); describe("with icon type elements", function() { beforeEach(function() { $view.append("") @@ -137,30 +184,18 @@ }); }) }); - describe("#getAllEls", function() { - var $els; + xdescribe("#click", function() { + it("would be annoying to test, but I manually tested this"); + }); + describe("#choose/from", function() { beforeEach(function() { - var form = $("
") - form.append("") - form.append("") - form.append("") - form.append("") - $view.append("
") - $view.append(form) - $els = Behave.getAllEls(); - }); - it("should create an object with jQ elements from the whole page", function() { - $els.accept_terms.attr('name').should.eql(Behave.find('accept_terms').attr('name')); - $els.first_name.attr('name').should.eql(Behave.find('first_name').attr('name')); - }); - it("should camelCase elements with attrs that are dash-cased", function() { - $els.couponContainer.should.be.an.Object - }); - it("should concatenate the text of label elements", function() { - $els.PracticeUrl.should.be.an.Object + Behave.view = $view = $(templates.dropdowns); }); - xit("should give each element a reload method", function() { - // Need to test this properly, but it works for our angular testing; + it("should select a dropdown", function() { + var coupon = Behave.choose('InactiveCoupon').from('coupons'); + coupon.val().should.eql('InactiveCoupon'); + coupon = Behave.choose('ActiveCoupon').from('coupons'); + coupon.val().should.eql('ActiveCoupon'); }); }); }());