Last active
August 6, 2017 04:08
-
-
Save elgalu/cdab6850800483b244e5 to your computer and use it in GitHub Desktop.
Some Protractor - Jasmine 1.3.x custom matchers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
////////////////////////// | |
// Some Custom Matchers // | |
////////////////////////// | |
"use strict"; | |
// Usage: | |
// Add `require('./customMatchers.js');` in your onPrepare block or file | |
// Config | |
var specTimeoutMs = 10000; // 10 secs | |
// Helpers | |
function _refreshPage() { | |
// Swallow useless refresh page webdriver errors | |
browser.navigate().refresh().then(null, function(){}); | |
}; | |
/** | |
* Custom Jasmine matcher that waits for an element to be present and visible | |
* @param {Boolean} expectation Is always true since a falsy value doesn't make sense | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to find | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toBeReadyFnBuilder(builderTypeStr) { | |
return function toBeReady(exp) { | |
exp = (exp == null ? true : false); | |
if (!exp) throw new Error( | |
"This custom matcher doesn't support false expectation."); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
if (!elmFinderOrWebElm.element) throw new Error( | |
"This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to be present and visible. " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _isPresentError(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
return false; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
if (builderTypeStr === 'withRefresh') { | |
// Refresh page after more that some retries | |
if (driverWaitIterations > 7) { | |
_refreshPage(); | |
} | |
} | |
return elmFinderOrWebElm.isPresent(). | |
then(function isPresent(present) { | |
if (present) { | |
return elmFinderOrWebElm.isDisplayed(). | |
then(function isDisplayed(visible) { | |
lastWebdriverError = 'visible:' + visible; | |
return visible; | |
}, _isPresentError); | |
} else { | |
lastWebdriverError = 'present:' + present; | |
return false; | |
} | |
}, _isPresentError); | |
}, specTimeoutMs * 0.3).then(function(waitResult) { | |
return waitResult; | |
}, function(err) { | |
return _isPresentError(err); | |
}); | |
}; | |
}; | |
/** | |
* Custom Jasmine matcher builder that waits for an element to be enabled or disabled | |
* @param {Boolean} expectation Is always true since a falsy value doesn't make sense | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to find | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toBeEnabledOrDisabledFnBuilder(builderTypeStr) { | |
return function toBeEnabledOrDisabled(exp) { | |
exp = (exp == null ? true : false); | |
if (!exp) throw new Error( | |
"This custom matcher doesn't support false expectation."); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
if (!elmFinderOrWebElm.element) throw new Error( | |
"This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to be " + builderTypeStr + ". " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _isEnabledOrDisabledError(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
return false; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return elmFinderOrWebElm.isEnabled(). | |
then(function isEnabled(enabled) { | |
if (builderTypeStr === 'enabled') { | |
lastWebdriverError = 'enabled:' + enabled; | |
return enabled; | |
} else { | |
lastWebdriverError = 'disabled:' + !enabled; | |
return !enabled; | |
} | |
}, _isEnabledOrDisabledError); | |
}, specTimeoutMs * 0.3).then(function(waitResult) { | |
return waitResult; | |
}, function(err) { | |
return _isEnabledOrDisabledError(err); | |
}); | |
}; | |
}; | |
/** | |
* Custom Jasmine matcher builder that waits for an element to have | |
* or not have an html class. | |
* @param {String} expectation The html class name | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to find | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toHaveClassFnBuilder(builderTypeBool) { | |
return function toHaveClass(clsName) { | |
if (clsName == null) throw new Error( | |
"Custom matcher toHaveClass needs a class name"); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
// if (!elmFinderOrWebElm.element) throw new Error( | |
// "This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
var thisIsNot = customMatcherFnThis.isNot; | |
var testHaveClass = !thisIsNot; | |
if (!builderTypeBool) { | |
testHaveClass = !testHaveClass; | |
} | |
var haveOrNot = testHaveClass ? 'have' : 'not to have'; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to " + haveOrNot + | |
" class " + clsName + ". " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _haveClassOrNotError(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
return false; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return elmFinderOrWebElm.getAttribute('class'). | |
then(function getAttributeClass(classes) { | |
var hasClass = classes.split(' ').indexOf(clsName) !== -1; | |
if (testHaveClass) { | |
lastWebdriverError = 'class present:' + hasClass; | |
return hasClass; | |
} else { | |
lastWebdriverError = 'class absent:' + !hasClass; | |
return !hasClass; | |
} | |
}, _haveClassOrNotError); | |
}, specTimeoutMs * 0.3).then(function(waitResult) { | |
if (thisIsNot) { | |
// Jasmine 1.3.1 expects to fail on negation | |
return !waitResult; | |
} else { | |
return waitResult; | |
} | |
}, function(err) { | |
// Jasmine 1.3.1 expects to fail on negation | |
return thisIsNot; | |
}); | |
}; | |
}; | |
/** | |
* Custom Jasmine matcher builder that waits for an element to have or not | |
* have an html attribute with optionally specifying its value | |
* @param {String} attribute The attribute to check for presence | |
* @param {String} opt_value The optional attribute value to also validate | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to find | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toHaveAttributeFnBuilder(builderTypeBool) { | |
return function toHaveAttribute(attribute, opt_attrValue) { | |
if (attribute == null) throw new Error( | |
"Custom matcher toHaveAttribute needs an attribute name"); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
// if (!elmFinderOrWebElm.element) throw new Error( | |
// "This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
var thisIsNot = customMatcherFnThis.isNot; | |
var testHaveAttr = !thisIsNot; | |
if (!builderTypeBool) { | |
testHaveAttr = !testHaveAttr; | |
} | |
var haveOrNot = testHaveAttr ? 'have' : 'not to have'; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to " + haveOrNot + | |
" attribute: '" + attribute + "'. " + | |
(opt_attrValue ? | |
"With value: '" + opt_attrValue + "'. " : '') + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _haveAttributeOrNotErr(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
return false; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return elmFinderOrWebElm.getAttribute(attribute). | |
then(function getAttribute(value) { | |
if (testHaveAttr) { | |
if (opt_attrValue == null) { | |
return (value !== null); | |
} else { | |
lastWebdriverError = "attribute value: '" + value + "'"; | |
return (value === opt_attrValue); | |
} | |
} else { | |
if (opt_attrValue == null) { | |
return (value === null); | |
} else { | |
lastWebdriverError = "attribute value: '" + value + "'"; | |
return (value !== opt_attrValue); | |
} | |
} | |
}, _haveAttributeOrNotErr); | |
}, specTimeoutMs * 0.3).then(function(waitResult) { | |
if (thisIsNot) { | |
// Jasmine 1.3.1 expects to fail on negation | |
return !waitResult; | |
} else { | |
return waitResult; | |
} | |
}, function(err) { | |
// Jasmine 1.3.1 expects to fail on negation | |
return thisIsNot; | |
}); | |
}; | |
}; | |
/** | |
* Custom Jasmine matcher that waits for an element not to be present or at | |
* least to not to be visible. | |
* @param {Boolean} expectation Is always true since a falsy value doesn't make sense | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to check for existence or invisibility | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toBeAbsent(exp) { | |
exp = (exp == null ? true : false); | |
if (!exp) throw new Error( | |
"This custom matcher doesn't support false expectation."); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
if (!elmFinderOrWebElm.element) throw new Error( | |
"This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to be absent or at least not visible. " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _isPresentError(err) { | |
var ret = false; | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
try { | |
var lastErr0 = lastWebdriverError.split(':')[0].trim(); | |
var lastErr1 = lastWebdriverError.split(':')[1].trim(); | |
ret = (lastErr0 === 'NoSuchElementError' || | |
lastErr1 === 'No element found using locator'); | |
} catch(e) {} | |
return ret; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return elmFinderOrWebElm.isPresent(). | |
then(function isPresent(present) { | |
if (present) { | |
return elmFinderOrWebElm.isDisplayed(). | |
then(function isDisplayed(visible) { | |
lastWebdriverError = 'visible:' + visible; | |
return !visible; | |
}, _isPresentError); | |
} else { | |
lastWebdriverError = 'present:' + present; | |
return true; | |
} | |
}, _isPresentError); | |
}, specTimeoutMs * 0.4).then(function(waitResult) { | |
return waitResult; | |
}, function(err) { | |
return _isPresentError(err); | |
}); | |
}; | |
/** | |
* Custom Jasmine matcher that validates JS data type loosely. | |
* @param {Type} expType The expected type, e.g. Object, Array, String | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The actual value to check typing | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toBeAn(expType) { | |
if (expType == null) throw new Error( | |
"This custom matcher needs an expected type."); | |
var customMatcherFnThis = this; | |
var actualValue = customMatcherFnThis.actual; | |
if (actualValue == null) throw new Error( | |
"<actual> can not be undefined."); | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
var thisIsNot = customMatcherFnThis.isNot; | |
var toBeOrNot = thisIsNot ? 'not to be' : 'to be'; | |
customMatcherFnThis.message = function() { | |
var typeName = (expType.name || expType.toString()); | |
return "Expected <" + actualValue.toString() + "> " + | |
toBeOrNot + " a kind of " + typeName; | |
}; | |
return ( (actualValue instanceof expType) || (expType.name && | |
expType.name.toLowerCase() === typeof actualValue) ); | |
}; | |
/** | |
* Custom Jasmine matcher that waits for a dropdown to have an specific | |
* option selected | |
* @param {String} exp The expected inner html with the value option | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {ElementFinder} this.actual The element to check selected option | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
*/ | |
function toHaveSelectedOption(exp) { | |
if (exp == null) throw new Error( | |
"Argument error: expectation string needed but got: " + exp); | |
var customMatcherFnThis = this; | |
var elmFinderOrWebElm = customMatcherFnThis.actual; | |
if (!elmFinderOrWebElm) throw new Error( | |
"<actual> can not be undefined."); | |
if (!elmFinderOrWebElm.element) throw new Error( | |
"This custom matcher only works on an actual ElementFinder."); | |
var driverWaitIterations = 0; | |
var lastWebdriverError; | |
customMatcherFnThis.message = function() { | |
var msg; | |
if (elmFinderOrWebElm.locator) { | |
msg = elmFinderOrWebElm.locator().toString(); | |
} else { | |
msg = elmFinderOrWebElm.toString(); | |
} | |
return "Expected '" + msg + "' to have selected option: '" + | |
exp + "'. " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last webdriver error: " + lastWebdriverError + "."; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _innerHtmlError(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
browser.sleep(500); | |
return false; | |
}; | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return elmFinderOrWebElm.getInnerHtml(). | |
then(function getInnerHtml(actual) { | |
if (actual === exp) { | |
return true; | |
} else { | |
return _innerHtmlError( | |
"getInnerHtml actual value: '" + actual + "'"); | |
} | |
}, _innerHtmlError); | |
}, specTimeoutMs * 0.4).then(function(waitResult) { | |
return waitResult; | |
}, function(err) { | |
return _innerHtmlError(err); | |
}); | |
}; | |
/** | |
* Custom Jasmine matcher that waits for a url to be or become the expected | |
* @param {String} exp The expected url string property name | |
* @return {Boolean} Returns the expectation result | |
* | |
* Uses the following object properties: | |
* {String} this.actual The url string promise. | |
* Creates the following object properties: | |
* {String} this.message The error message to show | |
* {Error} this.spec.lastStackTrace A better stack trace of user's interest | |
* | |
* Example | |
* expect(browser.getUrl()).toMatchRoute('privacyPolicy'); | |
*/ | |
function toMatchRoute(expRouteKey) { | |
var customMatcherFnThis = this; | |
if (browser.params.routes == null) throw new Error( | |
'Needed: browser.params.routes for this matcher'); | |
if (expRouteKey == null || typeof expRouteKey !== 'string') throw new | |
Error("Argument error: expectation string needed but got: " + | |
expRouteKey); | |
var actualUrl = customMatcherFnThis.actual; | |
if (actualUrl == null) throw new Error( | |
'<actual> can not be undefined or null'); | |
if (typeof actualUrl !== 'string') throw new Error( | |
'<actual> should have been resolved to a string but was: ' | |
+ actualUrl); | |
var urlMatcher = browser.params.routes.buildMatcher(expRouteKey); | |
var driverWaitIterations = 0; | |
var lastUrlFound; | |
var lastWebdriverError; | |
customMatcherFnThis.message = function() { | |
var msg; | |
return "Expected url to match: " + urlMatcher.toString() + ". " + | |
"After " + driverWaitIterations + " driverWaitIterations. " + | |
"Last url found: '" + lastUrlFound + "'. " + | |
"Last webdriver error: " + lastWebdriverError + "."; | |
}; | |
// This will be picked up by elgalu/jasminewd#jasmine_retry | |
customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); | |
function _retryOnErr(err) { | |
lastWebdriverError = (err != null) ? err.toString() : err; | |
return false; | |
}; | |
if (urlMatcher.test(actualUrl)) | |
return true; // all done | |
return browser.driver.wait(function() { | |
driverWaitIterations++; | |
return browser.getUrl().then(function(url) { | |
if (urlMatcher.test(url)) { | |
return true; | |
} else { | |
lastUrlFound = url; | |
return _retryOnErr(); | |
} | |
}, _retryOnErr); | |
}, specTimeoutMs * 0.4).then(function(waitRetValue) { | |
return waitRetValue; | |
}, function(err) { | |
return _retryOnErr(err); | |
}); | |
}; | |
// Add the custom matchers to jasmine | |
beforeEach(function() { | |
this.addMatchers({ | |
toBePresentAndDisplayed: toBeReadyFnBuilder(), | |
toBeReady: toBeReadyFnBuilder(), | |
toBeReadyWithRefresh: toBeReadyFnBuilder('withRefresh'), | |
toBeEnabled: toBeEnabledOrDisabledFnBuilder('enabled'), | |
toBeDisabled: toBeEnabledOrDisabledFnBuilder('disabled'), | |
toBeAbsent: toBeAbsent, | |
toHaveClass: toHaveClassFnBuilder(true), | |
toNotHaveClass: toHaveClassFnBuilder(false), | |
toHaveSelectedOption: toHaveSelectedOption, | |
toHaveAttribute: toHaveAttributeFnBuilder(true), | |
toNotHaveAttribute: toHaveAttributeFnBuilder(false), | |
toBeAn: toBeAn, | |
toBeA: toBeAn, | |
toMatchRoute: toMatchRoute, | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it('tests the custom matchers', function() { | |
// Wait up to 10secs for the element to appear and become visible | |
// it even swallows uncomfortable errors like StaleElementError | |
expect($('#user_name')).toBePresentAndDisplayed(); | |
// a shorter alias | |
expect($('#user_name')).toBeReady(); | |
// These guys should pass OK given your user input | |
// element starts with an ng-invalid class: | |
expect($('#user_name')).toHaveClass('ng-invalid'); | |
expect($('#user_name')).not.toHaveClass('ZZZ'); | |
expect($('#user_name')).toNotHaveClass('ZZZ'); | |
expect($('#user_name')).not.toNotHaveClass('ng-invalid'); | |
// These guys should each fail: | |
expect($('#user_name')).toHaveClass('ZZZ'); | |
expect($('#user_name')).not.toHaveClass('ng-invalid'); | |
expect($('#user_name')).toNotHaveClass('ng-invalid'); | |
expect($('#user_name')).not.toNotHaveClass('ZZZ'); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment