UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

1,237 lines (1,003 loc) 69 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test.More A mixin with additional generic assertion methods, which can work cross-platform between browsers and NodeJS. Is being consumed by {@link Siesta.Test}, so all of them are available in all tests. */ Role('Siesta.Test.More', { requires : [ 'isFailed', 'typeOf', 'on' ], has : { autoCheckGlobals : false, expectedGlobals : Joose.I.Array, disableGlobalsCheck : false, browserGlobals : { init : [ 'console', 'getInterface', 'ExtBox1', '__IE_DEVTOOLBAR_CONSOLE_COMMAND_LINE', /__BROWSERTOOLS/, // IE11 with console open 'seleniumAlert', 'onload', 'onerror', 'StartTest', 'startTest', '__loaderInstrumentationHookInstalled__', 'describe', // will be reported in IE8 after overriding 'setTimeout', 'clearTimeout', 'requestAnimationFrame', 'cancelAnimationFrame', '__coverage__', /cov_\w+/ ] }, /** * @cfg {Number} waitForTimeout Default timeout for `waitFor` (in milliseconds). Default value is 10000. */ waitForTimeout : 10000, waitForPollInterval : 100, suppressPassedWaitForAssertion : false }, methods : { /** * This assertion passes, when the comparison of 1st with 2nd, using `>` operator will return `true` and fails otherwise. * * @param {Number/Date} value1 The 1st value to compare * @param {Number/Date} value2 The 2nd value to compare * @param {String} [desc] The description of the assertion */ isGreater : function (value1, value2, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (value1 > value2) this.pass(desc, { descTpl : R.get('isGreaterPassTpl'), value1 : value1, value2 : value2 }) else this.fail(desc, { assertionName : 'isGreater', got : value1, need : value2, needDesc : R.get('needGreaterThan') }) }, /** * This assertion passes, when the comparison of 1st with 2nd, using `<` operator will return `true` and fails otherwise. * * @param {Number/Date} value1 The 1st value to compare * @param {Number/Date} value2 The 2nd value to compare * @param {String} [desc] The description of the assertion */ isLess : function (value1, value2, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (value1 < value2) this.pass(desc, { descTpl : R.get('isLessPassTpl'), value1 : value1, value2 : value2 }) else this.fail(desc, { assertionName : 'isLess', got : value1, need : value2, needDesc : R.get('needLessThan') }) }, isGE : function () { this.isGreaterOrEqual.apply(this, arguments) }, /** * This assertion passes, when the comparison of 1st with 2nd, using `>=` operator will return `true` and fails otherwise. * * It has a synonym - `isGE`. * * @param {Number/Date} value1 The 1st value to compare * @param {Number/Date} value2 The 2nd value to compare * @param {String} [desc] The description of the assertion */ isGreaterOrEqual : function (value1, value2, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (value1 >= value2) this.pass(desc, { descTpl : R.get('isGreaterEqualPassTpl'), value1 : value1, value2 : value2 }) else this.fail(desc, { assertionName : 'isGreaterOrEqual', got : value1, need : value2, needDesc : R.get('needGreaterEqualTo') }) }, isLE : function () { this.isLessOrEqual.apply(this, arguments) }, /** * This assertion passes, when the comparison of 1st with 2nd, using `<=` operator will return `true` and fails otherwise. * * It has a synonym - `isLE`. * * @param {Number/Date} value1 The 1st value to compare * @param {Number/Date} value2 The 2nd value to compare * @param {String} [desc] The description of the assertion */ isLessOrEqual : function (value1, value2, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (value1 <= value2) this.pass(desc, { descTpl : R.get('isLessEqualPassTpl'), value1 : value1, value2 : value2 }) else this.fail(desc, { assertionName : 'isLessOrEqual', got : value1, need : value2, needDesc : R.get('needLessEqualTo') }) }, /** * This assertion suppose to compare the numeric values. It passes when the passed values are approximately the same (the difference * is withing a threshold). A threshold can be provided explicitly (when assertion is called with 4 arguments), * or it will be set to 5% from the 1st value (when calling assertion with 3 arguments). * * @param {Number} value1 The 1st value to compare * @param {Number} value2 The 2nd value to compare * @param {Number} threshold The maximum allowed difference between values. This argument can be omitted. * @param {String} [desc] The description of the assertion */ isApprox : function (value1, value2, threshold, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (arguments.length == 2) threshold = Math.abs(value1 * 0.05) if (arguments.length == 3) { if (this.typeOf(threshold) == 'String') { desc = threshold threshold = Math.abs(value1 * 0.05) } } // this function normalizes the fractional numbers to fixed point presentation // for example in JS: 1.05 - 1 = 0.050000000000000044 // so what we do is: (1.05 * 10^2 - 1 * 10^2) / 10^2 = (105 - 100) / 100 = 0.05 var subtract = function (value1, value2) { var fractionalLength = function (v) { var afterPointPart = (v + '').split('.')[ 1 ] return afterPointPart && afterPointPart.length || 0 } var maxLength = Math.max(fractionalLength(value1), fractionalLength(value2)) var k = Math.pow(10, maxLength); return (value1 * k - value2 * k) / k; }; if (Math.abs(subtract(value2, value1)) <= threshold) this.pass(desc, { descTpl : R.get('isApproxToPassTpl'), value1 : value1, value2 : value2, annotation : value2 == value1 ? R.get('exactMatch') : (R.get('withinThreshold') + ': ' + threshold) }) else this.fail(desc, { assertionName : 'isApprox', got : value1, need : value2, needDesc : R.get('needApprox'), annotation : R.get('thresholdIs') + ': ' + threshold }) }, /** * This assertion passes when the passed `string` matches to a regular expression `regex`. When `regex` is a string, * assertion will check that it is a substring of `string` * * @param {String} string The string to check for "likeness" * @param {String/RegExp} regex The regex against which to test the string, can be also a plain string * @param {String} [desc] The description of the assertion */ like : function (string, regex, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(regex) == "RegExp") if (string.match(regex)) this.pass(desc, { descTpl : R.get('stringMatchesRe'), string : string, regex : regex }) else this.fail(desc, { assertionName : 'like', got : string, need : regex, needDesc : R.get('needStringMatching') }) else if (string.indexOf(regex) != -1) this.pass(desc, { descTpl : R.get('stringHasSubstring'), string : string, regex : regex }) else this.fail(desc, { assertionName : 'like', got : string, need : regex, needDesc : R.get('needStringContaining') }) }, /** * This method is the opposite of 'like', it adds failed assertion, when the string matches the passed regex. * * @param {String} string The string to check for "unlikeness" * @param {String/RegExp} regex The regex against which to test the string, can be also a plain string * @param {String} [desc] The description of the assertion */ unlike : function(string, regex, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(regex) == "RegExp") if (!string.match(regex)) this.pass(desc, { descTpl : R.get('stringNotMatchesRe'), string : string, regex : regex }) else this.fail(desc, { assertionName : 'unlike', got : string, need : regex, needDesc : R.get('needStringNotMatching') }) else if (string.indexOf(regex) == -1) this.pass(desc, { descTpl : R.get('stringHasNoSubstring'), string : string, regex : regex }) else this.fail(desc, { assertionName : 'unlike', got : string, need : regex, needDesc : R.get('needStringNotContaining') }) }, "throws" : function () { this.throwsOk.apply(this, arguments) }, throws_ok : function () { this.throwsOk.apply(this, arguments) }, /** * This assertion passes if the `func` function throws an exception during executing, and the * stringified exception passes the 'like' assertion (with 'expected' parameter). * * It has synonyms - `throws_ok` and `throws`. * * t.throwsOk(function(){ * throw "oopsie"; * }, 'oopsie', 'Some description text'); * * See also {@link Siesta.Test#livesOk} method. * * @param {Function} func The function which should throw an exception * @param {String/RegExp} expected The regex against which to test the stringified exception, can be also a plain string * @param {String} [desc] The description of the assertion */ throwsOk : function (func, expected, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(func) != 'Function') throw new Error(R.get('throwsOkInvalid')) var e = this.getExceptionCatcher()(func) // assuming no one will throw undefined exception.. if (e === undefined) { this.fail(desc, { assertionName : 'throws_ok', annotation : R.get('didntThrow') }) return } if (e instanceof this.getTestErrorClass()) //IE uses non-standard 'description' property for error msg e = e.message || e.description e = '' + e if (this.typeOf(expected) == "RegExp") if (e.match(expected)) this.pass(desc, { descTpl : R.get('exMatchesRe'), expected : expected }) else this.fail(desc, { assertionName : 'throws_ok', got : e, gotDesc : R.get('exceptionStringifiesTo'), need : expected, needDesc : R.get('needStringMatching') }) else if (e.indexOf(expected) != -1) this.pass(desc, { descTpl : R.get('exContainsSubstring'), expected : expected }) else this.fail(desc, { assertionName : 'throws_ok', got : e, gotDesc : R.get('exceptionStringifiesTo'), need : expected, needDesc : R.get('needStringContaining') }) }, lives_ok : function () { this.livesOk.apply(this, arguments) }, lives : function () { this.livesOk.apply(this, arguments) }, /** * This assertion passes, when the supplied `func` function doesn't throw an exception during execution. * * See also {@link Siesta.Test#throwsOk} method. * * This method has two synonyms: `lives_ok` and `lives` * * @param {Function} func The function which is not supposed to throw an exception * @param {String} [desc] The description of the assertion */ livesOk : function (func, desc) { if (this.typeOf(func) != 'Function') { func = [ desc, desc = func ][ 0 ] } var R = Siesta.Resource('Siesta.Test.More'); var e = this.getExceptionCatcher()(func) if (e === undefined) this.pass(desc, { descTpl : R.get('fnDoesntThrow') }) else this.fail(desc, { assertionName : 'lives_ok', annotation : R.get('fnThrew') + ': ' + e }) }, isa_ok : function (value, className, desc) { this.isInstanceOf(value, className, desc) }, isaOk : function (value, className, desc) { this.isInstanceOf(value, className, desc) }, /** * This assertion passes, when the supplied `value` is the instance of the `className`. The check is performed with * `instanceof` operator. The `className` parameter can be supplied as class constructor or as string, representing the class * name. In the latter case the `class` will eval'ed to receive the class constructor. * * This method has synonyms: `isaOk`, `isa_ok` * * @param {Mixed} value The value to check for 'isa' relationship * @param {Class/String} className The class to check for 'isa' relationship with `value` * @param {String} [desc] The description of the assertion */ isInstanceOf : function (value, className, desc) { var R = Siesta.Resource('Siesta.Test.More'); try { if (this.typeOf(className) == 'String') className = this.global.eval(className) } catch (e) { this.fail(desc, { assertionName : 'isa_ok', annotation : Siesta.Resource('Siesta.Test.Function', 'exceptionEvalutingClass') }) return } if (value instanceof className) this.pass(desc, { descTpl : R.get('isInstanceOfPass') }) else this.fail(desc, { assertionName : 'isa_ok', got : value, need : String(className), needDesc : R.get('needInstanceOf') }) }, /** * This assertion passes, if supplied value is a String. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isString : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'String') this.pass(desc, { descTpl : R.get('isAString'), value : value }) else this.fail(desc, { got : value, need : R.get('aStringValue') }) }, /** * This assertion passes, if supplied value is an Object * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isObject : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Object') this.pass(desc, { descTpl : R.get('isAnObject'), value : value }) else this.fail(desc, { got : value, need : R.get('anObject') }) }, /** * This assertion passes, if supplied value is an Array * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isArray : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Array') this.pass(desc, { descTpl : R.get('isAnArray'), value : value }) else this.fail(desc, { got : value, need : R.get('anArrayValue') }) }, /** * This assertion passes, if supplied value is a Number. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isNumber : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Number') this.pass(desc, { descTpl : R.get('isANumber'), value : value }) else this.fail(desc, { got : value, need : R.get('aNumberValue') }) }, /** * This assertion passes, if supplied value is a Boolean. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isBoolean : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Boolean') this.pass(desc, { descTpl : R.get('isABoolean'), value : value }) else this.fail(desc, { got : value, need : R.get('aBooleanValue') }) }, /** * This assertion passes, if supplied value is a Date. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isDate : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Date') this.pass(desc, { descTpl : R.get('isADate'), value : value }) else this.fail(desc, { got : value, need : R.get('aDateValue') }) }, /** * This assertion passes, if supplied value is a RegExp. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isRegExp : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'RegExp') this.pass(desc, { descTpl : R.get('isARe'), value : value }) else this.fail(desc, { got : value, need : R.get('aReValue') }) }, /** * This assertion passes, if supplied value is a Function. * * @param {Mixed} value The value to check. * @param {String} [desc] The description of the assertion */ isFunction : function (value, desc) { var R = Siesta.Resource('Siesta.Test.More'); if (this.typeOf(value) == 'Function' || this.typeOf(value) == 'AsyncFunction') this.pass(desc, { descTpl : R.get('isAFunction'), value : value }) else this.fail(desc, { got : value, need : R.get('aFunctionValue') }) }, is_deeply : function (obj1, obj2, desc) { this.isDeeply.apply(this, arguments) }, /** * This assertion passes when in-depth comparison of 1st and 2nd arguments (which are assumed to be JSON objects) shows that they are equal. * Comparison is performed with '==' operator, so `[ 1 ]` and `[ "1" ] objects will be equal. The objects should not contain cyclic references. * * This method works correctly with the *placeholders* generated with method {@link #any}. * * This method has a synonym: `is_deeply` * * @param {Object} obj1 The 1st object to compare * @param {Object} obj2 The 2nd object to compare * @param {String} [desc] The description of the assertion */ isDeeply : function (obj1, obj2, desc) { var R = Siesta.Resource('Siesta.Test.More'); var diff if (this.typeOf(obj1) === this.typeOf(obj2) && this.compareObjects(obj1, obj2)) { this.pass(desc, { descTpl : R.get('isDeeplyPassTpl'), obj1 : obj1, obj2 : obj2 }) } // DeepDiff Not supported in IE8 else if (typeof DeepDiff != 'undefined' && (diff = DeepDiff(obj1, obj2))) { if (diff.length > 5) { this.diag(R.get('tooManyDifferences', { num : 5, total : diff.length})) } for (var i = 0; i < Math.min(diff.length, 5); i++) { var diffItem = diff[i]; var path = (diffItem.path || []).join('.'); var saw = path ? (path + ': ' + diffItem.lhs) : obj1; var expected = path ? (path + ': ' + diffItem.rhs) : obj2; this.fail(desc, { assertionName : 'isDeeply', got : saw, need : expected }) // Also log it to console for easy inspection window.console && console.log('DIFF RESULT:', diffItem); } } else { this.fail(desc, { assertionName : 'isDeeply', got : obj1, need : obj2 }) } }, /** * This assertion passes when in-depth comparison of 1st and 2nd arguments (which are assumed to be JSON objects) shows that they are equal. * Comparison is performed with '===' operator, so `[ 1 ]` and `[ "1" ] objects will be different. The objects should not contain cyclic references. * * This method works correctly with the *placeholders* generated with method {@link #any}. * * @param {Object} obj1 The 1st object to compare * @param {Object} obj2 The 2nd object to compare * @param {String} [desc] The description of the assertion */ isDeeplyStrict : function (obj1, obj2, desc) { if (this.typeOf(obj1) === this.typeOf(obj2) && this.compareObjects(obj1, obj2, true)) { var R = Siesta.Resource('Siesta.Test.More'); this.pass(desc, { descTpl : R.get('isDeeplyStrictPassTpl'), obj1 : obj1, obj2 : obj2 }) } else this.fail(desc, { assertionName : 'isDeeplyStrict', got : obj1, need : obj2 }) }, expectGlobal : function () { this.expectGlobals.apply(this, arguments) }, /** * This method accepts a variable number of names of expected properties in the global scope. When verifying the globals with {@link #verifyGlobals} * assertions, the expected gloabls will not be counted as failed assertions. * * This method has a synonym with singular name: `expectGlobal` * * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties */ expectGlobals : function () { this.expectedGlobals.push.apply(this.expectedGlobals, arguments) }, isGlobalExpected : function (name, index) { var me = this if (!index || index && !index.expectedStrings) { if (!index) index = {} Joose.O.extend(index, { expectedStrings : {}, expectedRegExps : [] }) Joose.A.each(this.expectedGlobals.concat(this.browserGlobals), function (value) { if (me.typeOf(value) == 'RegExp') index.expectedRegExps.push(value) else index.expectedStrings[ value ] = true }) } if (index.expectedStrings[ name ]) return true var imageWithIdCreatesGlobalEnumerable = Siesta.Project.Browser.FeatureSupport().supports.imageWithIdCreatesGlobalEnumerable; // remove after https://bugzilla.mozilla.org/show_bug.cgi?id=959992 will be fixed if (imageWithIdCreatesGlobalEnumerable) { var domEl = this.global.document.getElementById(name) if (domEl && domEl.tagName.toLowerCase() == 'img') return true; } for (var i = 0; i < index.expectedRegExps.length; i++) if (index.expectedRegExps[ i ].test(name)) return true return false }, forEachUnexpectedGlobal : function (func, scope) { scope = scope || this var index = {} for (var name in this.global) if (!this.isGlobalExpected(name, index)) { if (func.call(scope, name) === false) { break; } } }, /** * This method accepts a variable number of names of expected properties in the global scope and then performs a globals check. * * It will scan all globals properties in the scope of test and compare them with the list of expected globals. Expected globals can be provided with: * {@link #expectGlobals} method or {@link Siesta.Project#expectedGlobals expectedGlobals} configuration option of project. * * You can enable this assertion to automatically happen at the end of each test, using {@link Siesta.Project#autoCheckGlobals autoCheckGlobals} option of the project. * * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties */ verifyGlobals : function () { var R = Siesta.Resource('Siesta.Test.More'); if (this.disableGlobalsCheck) { this.diag(R.get('globalCheckNotSupported')); return } this.expectGlobals.apply(this, arguments) this.diag(R.get('globalVariables')) var failed = false var i = 0 this.forEachUnexpectedGlobal(function (name) { this.fail( R.get('globalFound'), R.get('globalName') + ': ' + name + ', ' + R.get('value') + ': ' + Siesta.Util.Serializer.stringify(this.global[ name ]) ) failed = true return i++ < 50 // Only report first 50 globals to protect against legacy apps with thousands of globals }) if (!failed) this.pass(R.get('noGlobalsFound')) }, // will create a half-realized, "phantom", "isWaitFor" assertion, which is only purposed // for user to get the instant feedback about "waitFor" actions // this assertion will be "finalized" and added to the test results in the "finalizeWaiting" startWaiting : function (description, sourceLine) { var result = new Siesta.Result.Assertion({ description : description, isWaitFor : true, sourceLine : sourceLine }); this.fireEvent('testupdate', this, result, this.getResults()) return result; }, finalizeWaiting : function (result, passed, desc, annotation, errback, suppressPassedWaitForAssertion) { // Treat this is an ordinary assertion from now on result.completed = true; if (passed) { if (this.suppressPassedWaitForAssertion || suppressPassedWaitForAssertion) { // Make sure UI is updated and the "noise" is removed this.fireEvent('assertiondiscard', this, result) } else { this.pass(desc, annotation, result) } } else { this.fail(desc, annotation, result); errback && errback() } }, conditionCheckerToString : function (checker) { if (this.typeOf(checker) !== 'Function') return '' var sources = checker.toString().split('\n') var minCommonLeadingWhitespace = Infinity Joose.A.each(sources, function (line, index) { // ignore first line, which won't have the common leading whitespace if (index === 0) return var leadingWhitespaceMatch = /^(\s*)/.exec(line) if (leadingWhitespaceMatch) { var leadingWhitespace = leadingWhitespaceMatch[ 1 ] // ignore whitespace-only lines if (leadingWhitespace === line) return if (leadingWhitespace.length < minCommonLeadingWhitespace) minCommonLeadingWhitespace = leadingWhitespace.length } }) if (minCommonLeadingWhitespace < Infinity) Joose.A.each(sources, function (line, index) { // ignore first line, which won't have the common leading whitespace if (index === 0) return sources[ index ] = line.slice(minCommonLeadingWhitespace) }) return '[code]' + sources.join('\n') + '[/code]' }, /** * Waits for passed checker method to return true (or any non-false value, like for example DOM element or array), and calls the callback when this happens. * As an additional feature, the callback will receive the result from the checker method as the 1st argument. * t.waitFor( function () { return document.getElementById('someEl') }, function (el) { // waited for element #someEl to appear // element will be available in the callback as 1st argument "el" } ) * You can also call this method with a single Object having the following properties: `method`, `callback`, `scope`, `timeout`, `interval`, `description`: t.waitFor({ method : function () { return document.getElementById('someEl') }, callback : function (el) { // waited for element #someEl to appear // element will be available in the callback as 1st argument "el" } }) * * @param {Function/Number/Object} condition Either a function which should return true (or any other "truthy" value) when a certain condition has been fulfilled, * or a number of ms to wait before calling the callback. Can be also an object with the following properties: * @param {Function} condition.callback A function to call when the condition has been met. Will receive a result from checker function. * @param {Function} condition.method A condition checker function. * @param {Object} condition.scope The scope for the callback. * @param {Number} condition.timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled. * @param {Number} condition.interval The polling interval (in milliseconds) * @param {String} condition.description The assertion description * * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function. * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled. * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test. * @param {Int} [interval=100] The polling interval (in milliseconds) * * @return {Promise} A promise which will be resolved when wait completes (either successfully or by timeout). In case of successfull resolution * promise will be resolved to the result from the checker function. Additionally it has a `force` property as noted below. * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback). * No call to checker will be performed and callback will not receive a result from it. */ waitFor : function (method, callback, scope, timeout, interval) { var R = Siesta.Resource('Siesta.Test.More'); var description = ' ' + R.get('conditionToBeFulfilled'); var assertionName = 'waitFor'; var me = this; var sourceLine = me.getSourceLine(); var originalSetTimeout = me.originalSetTimeout; var originalClearTimeout = me.originalClearTimeout; var errback; var suppressAssertion; if (arguments.length === 1 && this.typeOf(method) == 'Object') { var options = method; method = options.method; callback = options.callback; scope = options.scope; timeout = options.timeout; interval = options.interval description = options.description || description; assertionName = options.assertionName || assertionName; suppressAssertion = options.suppressAssertion; // errback is called in case "waitFor" has failed errback = options.errback } else options = {} var isWaitingForTime = this.typeOf(method) == 'Number' callback = callback || function () {} description = isWaitingForTime ? (method + ' ' + R.get('ms')) : description; var pollTimeout // early notification about the started "waitFor" operation var waitAssertion = me.startWaiting(R.get('waitingFor') + ' ' + description, sourceLine); interval = interval || this.waitForPollInterval timeout = timeout || this.waitForTimeout var resolve var res = new Promise(function (resolution) { resolve = resolution }) // this async frame is not supposed to fail, because it's delayed to `timeout + 3 * interval` // failure supposed to be generated in the "pollFunc" and this async frame to be closed // however, in IE the async frame may end earlier than failure from "pollFunc" // in such case we report the same error as in "pollFunc" var async = this.beginAsync((isWaitingForTime ? method : timeout) + 3 * interval, function () { isDone = true originalClearTimeout(pollTimeout) me.finalizeWaiting(waitAssertion, false, R.get('waitedTooLong') + ': ' + description, { assertionName : assertionName, annotation : me.typeOf(options.annotation) === 'Function' ? options.annotation() : R.get('conditionNotFulfilled') + ' ' + timeout + R.get('ms') + '. \n\n' + me.conditionCheckerToString(method) }, errback, suppressAssertion) resolve() return true }) var isDone = false var beforeFinalizeListener // stop polling, if this test instance has finalized (probably because of exception) this.on('beforetestfinalize', beforeFinalizeListener = function () { if (!isDone) { isDone = true me.finalizeWaiting(waitAssertion, false, R.get('waitingAborted'), null, null, suppressAssertion); me.endAsync(async) originalClearTimeout(pollTimeout) } }, null, { single : true }) if (isWaitingForTime) { if (method < 0) { throw new Error('Cannot wait for a negative amount of time'); } pollTimeout = originalSetTimeout(function() { isDone = true me.un('beforetestfinalize', beforeFinalizeListener) me.finalizeWaiting(waitAssertion, true, R.get('Waited') + ' ' + method + ' ' + R.get('ms'), null, null, suppressAssertion || method === 0); me.endAsync(async); me.processCallbackFromTest(callback, [], scope || me) resolve() }, method); } else { var result; var startDate = new Date() var pollFunc = function () { var time = new Date() - startDate; if (time > timeout) { me.endAsync(async); isDone = true try { me.un('beforetestfinalize', beforeFinalizeListener) me.finalizeWaiting(waitAssertion, false, R.get('waitedTooLong') + ': ' + description, { assertionName : assertionName, annotation : me.typeOf(options.annotation) === 'Function' ? options.annotation() : R.get('conditionNotFulfilled') + ' ' + timeout + R.get('ms') + '. \n\n' + me.conditionCheckerToString(method) }, errback, suppressAssertion) } catch (e) { if (!/__SIESTA_TEST_EXIT_EXCEPTION__/.test(String(e))) throw e } resolve() return } try { result = method.call(scope || me); } catch (e) { me.endAsync(async); try { me.un('beforetestfinalize', beforeFinalizeListener) me.finalizeWaiting(waitAssertion, false, assertionName + ' ' + R.get('checkerException'), { assertionName : assertionName, annotation : me.stringifyException(e) }, errback, suppressAssertion) } catch (e) { if (!/__SIESTA_TEST_EXIT_EXCEPTION__/.test(String(e))) throw e } isDone = true resolve() return } if (result != null && result !== false) { me.endAsync(async); isDone = true me.un('beforetestfinalize', beforeFinalizeListener) me.finalizeWaiting( waitAssertion, true, R.get('Waited') + ' ' + time + ' ' + R.get('msFor') + ' ' + description, me.typeOf(options.annotation) === 'Function' ? options.annotation() : null, null, // always add assertion (set "suppress" to false), if user has provided description (suppressAssertion || time === 0) && !options.description ); me.processCallbackFromTest(callback, [ result ], scope || me) resolve(result) } else pollTimeout = originalSetTimeout(pollFunc, interval) } pollFunc() } res.force = function () { // wait operation already completed if (isDone) return isDone = true originalClearTimeout(pollTimeout) me.endAsync(async); me.un('beforetestfinalize', beforeFinalizeListener) me.finalizeWaiting(waitAssertion, true, R.get('forcedWaitFinalization') + ' ' + description, null, null, suppressAssertion); me.processCallbackFromTest(callback, [], scope || me) resolve() } return res }, /** * Waits for the number of a number millseconds and calls the callback when after waiting. This is just a convenience synonym for the {@link #waitFor} method. t.waitForMs(1500, callback) * * @param {Number} method The number of ms to wait before calling the callback. * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function. * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled. * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test. * @param {Int} [interval=100] The polling interval (in milliseconds) * * @return {Object} An object with the following properties: * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback). * No call to checker will be performed and callback will not receive a result from it. */ waitForMs : function() { return this.waitFor.apply(this, arguments); }, /** * Waits for the passed checker method to return true (or any non-false value, like for example DOM element or array), and calls the callback when this happens. * This is just a convenience synonym for the {@link #waitFor} method. * t.waitForFn(function() { return true; }, callback) * * @param {Function} fn The checker function. * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function. * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled. * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test. * @param {Int} [interval=100] The polling interval (in milliseconds) * * @return {Object} An object with the following properties: * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback). * No call to checker will be performed and callback will not receive a result from it. */ waitForFn : function() { return this.waitFor.apply(this, arguments); }, // takes the step function and tries to analyze if it is missing the call to "next" // returns "true" if "next" is used, analyzeChainStep : function (func) { var sources = func.toString() var isArrow = !sources.match(/^function/) var firstArg