Skip to content

Instantly share code, notes, and snippets.

@elgalu
Last active August 6, 2017 04:08
Show Gist options
  • Save elgalu/cdab6850800483b244e5 to your computer and use it in GitHub Desktop.
Save elgalu/cdab6850800483b244e5 to your computer and use it in GitHub Desktop.
Some Protractor - Jasmine 1.3.x custom matchers
//////////////////////////
// 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,
});
});
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