/* 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 if (isArrow) { if (sources.match(/^\(/)) // args wrapper in () firstArg = sources.match(/\(\s*(.*?)\s*(?:,|\))/)[ 1 ] else // single argument not wrapped in () firstArg = sources.match(/(.*?)\s*=>/)[ 1 ] } else { firstArg = sources.match(/function\s*[^(]*\(\s*(.*?)\s*(?:,|\))/)[ 1 ] } if (!firstArg) return false var body if (isArrow) body = sources.match(/=>\s*([\s\S]*)/)[ 1 ] else body = sources.match(/\{([\s\S]*)\}/)[ 1 ] return body.indexOf(firstArg) != -1 }, stringifyChainStep : function (step) { return this.typeOf(step).match(/^(Async)?Function$/) ? step.toString() : Siesta.Util.Serializer.stringify(step) }, /** * This method accepts a variable number of steps, either as individual arguments or as a single array containing them. Steps and arrays * of steps are handled just fine, and any step-arrays passed will be flattened. Each step should be either a function or configuration * object for {@link Siesta.Test.Action test actions}. These functions / actions will be executed in order. * * 1) For a function step, it will receive a callback as the 1st argument, to call when the step is completed. * As the 2nd and further arguments, the step function will receive the arguments passed to the previous callback. * * If a function step returns a promise, it should not call the callback, but instead Siesta will wait until the promise returned is * resolved / rejected. This plays nicely with the `async/await` functions: * let someAsyncOperation = t => new Promise((resolve, reject) => { setTimeout(() => { resolve("someValue") }, 300) }) t.it('Doing async stuff', t => { t.chain( // function step, that returns a promise (sugared with async/await) async () => { return await someAsyncOperation(t) }, // desugared version function (next, result) { return someAsyncOperation(t) } ) }) * * The last step will receive a no-op callback, which can be ignored or still called. **Note**, that last step is assumed to * complete synchronously! If you need to launch some asynchronous process in the last step, you may need to add another empty function step * to the end of the chain. * * 2) For `Siesta.Test.Action` objects, the callback will be called by the action class automatically, * there's no need to provide any callback manually. The configuration object should contain an "action" property, specifying the action class * along with other config options depending on the action class. For brevity, instead of using the "action" property, the configuration * object can contain the property corresponding to the action name itself, with the action's target (or even a test method with arguments). * See the following examples and also refer to the documentation of the action classes. * * If the configuration object will contain a `desc` property, a passing assertion with its value will be added to the test, after this step has completed. * * 3) If a step is a sub test instance, created with {@link #getSubTest} method, then the step will launch it. * * It's better to see how it works in action. For example, when using using only functions: t.chain( // function receives a callback as 1st argument function (next) { // we pass that callback to the "click" method t.click(buttonEl, next) }, function (next) { t.type(fieldEl, 'Something', next) }, function (next) { t.is(fieldEl.value == 'Something', 'Correct value in the field') // call the callback with some arguments next('foo', 'bar') }, // those arguments are now available as arguments of next step function (next, value1, value2) { t.is(value1, 'foo', 'The arguments for the callback are translated to the arguments of the step') t.is(value2, 'bar', 'The arguments for the callback are translated to the arguments of the step') } ) * * The same example, using action configuration objects for first 2 steps. For the list of available actions * please refer to the classes in the `Siesta.Test.Action` namespace. t.chain( { action : 'click', target : buttonEl, desc : "Clicked on the button" }, // or { click : buttonEl, desc : "Clicked on the button" }, { action : 'type', target : fieldEl, text : 'Something', desc : "Typed in the field" }, // or { type : 'Something', target : fieldEl, desc : "Typed in the field" }, { waitFor : 'Selector', args : '.selector' } // or, using Siesta.Test.Action.MethodCall notation: { waitForSelector : '.selector' } function (next) { t.is(fieldEl.value == 'Something', 'Correct value in the field') next('foo', 'bar') }, ... ) * Please note, that by default, each step is expected to complete within the {@link Siesta.Project#defaultTimeout} time. * You can change this with the `timeout` property of the step configuration object, allowing some steps to last longer. * Steps with sub-tests are expected to complete within {@link Siesta.Project#subTestTimeout}. * * In a special case, `action` property of the step configuration object can be a function. In this case you can also * provide a `timeout` property, otherwise this case is identical to using functions: * t.chain( { action : function (next) { ... }, // allow 50s for the function to call "next" before step will be considered timed-out timeout : 50000 }, ... ) * **Tip**: * * If a step is presented with a `null` or `undefined` value it will be ignored. Additionally, a step can be * an array of steps - all arrays passed to t.chain will be flattened. * * These tips allows us to implement conditional steps processing, like this: * var el1IsInDom = t.$('.some-class1')[ 0 ] var el2IsInDom = t.$('.some-class2')[ 0 ] t.chain( { click : '.some-other-el' }, el1IsInDom ? [ { click : el1IsInDom }, el2IsInDom ? [ { click : el1IsInDom } ] : null, ] : null, ... ) * * See also : {@link #chainForArray}. * * @param {Function/Object/Array} step1 The function to execute or action configuration, or an array of steps * @param {Function/Object} step2 The function to execute or action configuration * @param {Function/Object} stepN The function to execute or action configuration */ chain : function () { // inline any arrays in the arguments into one array var steps = this.flattenArray(arguments) var R = Siesta.Resource('Siesta.Test.More'); var nonEmpty = [] Joose.A.each(steps, function (step) { if (step) nonEmpty.push(step) }) steps = nonEmpty var len = steps.length // do nothing if (!len) return; var me = this var self = arguments.callee var queue = new Siesta.Util.Queue({ deferer : this.originalSetTimeout, deferClearer : this.originalClearTimeout, // some dirt - "chain" is generic method and does not know about `simulator`, which appears on browser level only interval : self.hasOwnProperty('actionDelay') ? self.actionDelay : (this.simulator ? this.simulator.actionDelay : 1), observeTest : this }) // hack to allow configuration of `actionDelay`... delete self.actionDelay var sourceLine = me.getSourceLine(); var args = [] Joose.A.each(steps, function (step, index) { var isLast = index == len - 1 queue.addAsyncStep({ processor : function (data) { var hasReturnedPromise = false var initStep = function (stepHasOwnAsyncFrame) { if (!stepHasOwnAsyncFrame) { var timeout = step.timeout || me.defaultTimeout // + 100 to allow `waitFor` steps (which will be waiting the `timeout` time) to // generate their own failures var async = me.beginAsync(timeout + 100, function () { me.fail( R.get('chainStepNotCompleted'), { sourceLine : sourceLine, annotation : R.get('stepNumber') + ': ' + (index + 1) + ' ' + R.get('oneBased') + (sourceLine ? ('\n' + R.get('atLine') + ': ' + sourceLine) : '') + '\nStep source: ' + me.stringifyChainStep(step), ownTextOnly : true } ) return true }) } return { next : function () { var self = arguments.callee var isExtraCall = false if (hasReturnedPromise) { // ignore the 1st "manual" call to callback if promise was returned if (self.__CALLED__ === 1) { self.__CALLED__ = 2; return } // but not the 2nd if (self.__CALLED__ === 2) isExtraCall = true } else { if (self.__CALLED__ === 1) isExtraCall = true } if (isExtraCall) me.fail(R.get('calledMoreThanOnce', { num : index + 1, line : sourceLine })) self.__CALLED__ = 1 if (!stepHasOwnAsyncFrame) me.endAsync(async) args = Array.prototype.slice.call(arguments) if (step.desc) me.pass(step.desc) data.next && data.next() }, async : async } } if (step instanceof Siesta.Test) { // do not try to launch sub-test if finalization has started if (!(me.finalizationStarted || me.endDate)) me.launchSubTest(step, initStep(true).next) } else if (me.typeOf(step).match(/^(Async)?Function$/) || me.typeOf(step.action).match(/^(Async)?Function$/)) { var func = me.typeOf(step).match(/^(Async)?Function$/) ? step : step.action var stepInitData = initStep(false) // if the last step is a function - then provide empty function as the "next" callback for it var next = isLast ? function () {} : stepInitData.next args.unshift(next) var result if (me.transparentEx) result = func.apply(me, args) else { var e = me.getExceptionCatcher()(function () { result = func.apply(me, args) }) if (e !== undefined) { // the "me.endAsync()" here is not enough, because for the t.beforeEach() // hook, we have a nested chain - one in the "launchSpecs", // 2nd in the "runBeforeSpecHooks", thats why in the following statement // we finalize the whole test, to avoid extra waiting for the // "chain step can not proceed" message // for `isLast` case `endAsync` will be done below if (!isLast) me.endAsync(stepInitData.async) me.failWithException(e, R.get('chainStepEx')) } } // if there's no returning value from the step, then it must call `next` to continue if (!result && !isLast && !me.analyzeChainStep(func)) me.fail(R.get('stepFn') + ' [' + func.toString() + '] ' + R.get('notUsingNext')) if (result) { hasReturnedPromise = true me.handleReturnedPromise( result, next, me.formatString( 'The promise returned from the chain step [' + me.stringifyChainStep(step) + '] did not resolve within {time}ms', { time : me.defaultTimeout } ) ) } // and finalize the async frame manually, as the "nextFunc" for last step will never be called if (isLast) { me.endAsync(stepInitData.async) if (step.desc) me.pass(step.desc) } } else if (me.typeOf(step) == 'String') { var action = new Siesta.Test.Action.Eval({ actionString : step, next : initStep(false).next, test : me }) action.process() } else { var action = Siesta.Test.ActionRegistry().create(step, me, args, initStep) action.process() } } }) }) queue.run() }, /** * This is a wrapper around the {@link #chain} method, which allows you to run the chain over the steps, generated from the elements * of some array. For example, if in some step of outer chain, we need to click the elements with ids, given as the array, we can do: * function (next) { var ids = [ 'button-1', 'button-2', 'button-3' ] t.chainForArray(ids, function (elId) { return { click : '#' + elId } }, next) } * * @param {Array} array An array with arbitrary elements * @param {Function} generator A function, which will be called for every element of the `array`. It should return * a chain step, generated from that element. This function can return an array of steps as well. If generator will return `null` or * `undefined` nothing will be added to the chain. * @param {Function} generator.el An element of the `array` * @param {Function} generator.index An index of the element * @param {Function} [callback] A function to call, once the chain is completed. */ chainForArray : function (array, generator, callback, reverse) { var me = this var steps = [] Joose.A[ reverse ? 'eachR' : 'each' ](array, function (el, index) { var res = generator.call(me, el, index) if (me.typeOf(res) == 'Array') steps.push.apply(steps, res) else if (res) steps.push(res) }) if (callback) steps.push(function () { me.processCallbackFromTest(callback) }) this.chain(steps) }, verifyExpectedNumber : function (actual, expected) { var operator = '==' if (this.typeOf(expected) == 'String') { var match = /([<>=]=?)\s*(\d+)/.exec(expected) var R = Siesta.Resource('Siesta.Test.Browser'); if (!match) throw new Error(R.get('wrongFormat') + ": " + expected) operator = match[ 1 ] expected = Number(match[ 2 ]) } switch (operator) { case '==' : return actual == expected case '<=' : return actual <= expected case '>=' : return actual >= expected case '<' : return actual < expected case '>' : return actual > expected } }, getMaximalTimeout : function () { return Math.max(this.waitForTimeout, this.defaultTimeout, this.subTestTimeout, this.timeout || 0, this.isReadyTimeout) } }, after : { onBeforeTestFinalize : function () { if (this.autoCheckGlobals && !this.isFailed() && !this.parent) this.verifyGlobals() } } }) //eof Siesta.Test.More