UNPKG

siesta-lite

Version:

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

1,339 lines (1,016 loc) 89 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test @mixin Siesta.Test.More @mixin Siesta.Test.Date @mixin Siesta.Test.Function @mixin Siesta.Test.BDD @mixin Siesta.Util.Role.CanCompareObjects `Siesta.Test` is a base testing class in Siesta hierarchy. It's not supposed to be created manually, instead the project will create it for you. This file is a reference only, for a getting start guide and manual please refer to the <a href="#!/guide/getting_started_browser">Siesta getting started in browser environment</a> guide. SYNOPSIS ======== StartTest(function(t) { t.diag("Sanity") t.ok($, 'jQuery is here') t.ok(Your.Project, 'My project is here') t.ok(Your.Project.Util, '.. indeed') setTimeout(function () { t.ok(true, "True is ok") }, 500) }) */ Class('Siesta.Test', { does : [ Siesta.Util.Role.CanFormatStrings, Siesta.Util.Role.CanGetType, Siesta.Util.Role.CanCompareObjects, Siesta.Util.Role.CanEscapeRegExp, Siesta.Test.More, Siesta.Test.Date, Siesta.Test.Function, Siesta.Test.BDD, JooseX.Observable, // quick "id" attribute, perhaps should be changed later Siesta.Util.Role.HasUniqueGeneratedId ], has : { name : null, uniqueId : function () { var holder = Siesta.Test holder.__UNIQUE_ID_GEN__ = holder.__UNIQUE_ID_GEN__ || 0 return ++holder.__UNIQUE_ID_GEN__ }, /** * @property url The url of this test, as given to the {@link Siesta.Project#start start} method. All subtests of some top-level test shares the same url. */ url : { required : true }, urlExtractRegex : { is : 'rwc', lazy : function () { return new RegExp(this.url.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1") + ':(\\d+)') } }, referenceUrl : null, assertPlanned : null, assertCount : 0, // whether this test contains only "todo" assertions isTodo : false, results : { lazy : function () { return new Siesta.Result.SubTest({ description : this.name || 'Root', test : this }) } }, run : null, startTestAnchor : null, exceptionCatcher : null, testErrorClass : null, // same number for the whole subtests tree generation : function () { return Math.random() }, launchId : null, parent : null, project : null, // backward compat - alias for `project` harness : null, /** * @cfg {Number} isReadyTimeout * * Timeout in milliseconds to wait for test start. Default value is 10000. See also {@link #isReady} */ isReadyTimeout : 10000, // indicates that a test has thrown an exception (not related to failed assertions) failed : false, failedException : null, // stringified exception failedExceptionType : null, // type of exception // start and end date are stored as numbers (new Date() - 0) // this is to allow sharing date instances between different contexts startDate : null, endDate : null, lastActivityDate : null, contentManager : null, // the scope provider for the context of the test page scopeProvider : null, // the context of the test page global : null, reusingSandbox : false, sandboxCleanup : true, sharedSandboxState : null, // the scope provider for the context of the test script // usually the same as the `scopeProvider`, but may be different in case of using `enablePageRedirect` option scriptScopeProvider : null, transparentEx : false, needDone : false, isDone : false, defaultTimeout : 15000, // a default timeout for sub tests subTestTimeout : null, // a timeout of this particular test timeout : null, timeoutsCount : function () { return { counter : 1 } }, timeoutIds : Joose.I.Object, idsToIndex : Joose.I.Object, waitTitles : Joose.I.Object, // indicates that test function has completed the execution (test may be still running due to async) processed : false, // indicates that test has started finalization process ("tearDown" method). At this point, test is considered // finished, but the failing assertion (if "tearDown" fails) may still be added finalizationStarted : false, callback : null, // Nbr of exceptions detected while running the test nbrExceptions : 0, testEndReported : false, // only used for testing itself, otherwise should be always `true` needToCleanup : true, overrideSetTimeout : false, overrideForSetTimeout : null, overrideForClearTimeout : null, originalSetTimeout : null, originalClearTimeout : null, sourceLineForAllAssertions : false, $passCount : null, $failCount : null, actionableMethods : { lazy : 'buildActionableMethods' }, jUnitClass : null, groups : null, automationElementId : null, // enableCodeCoverage : false, snoozeUntil : null, breakTestOnFail : false, breakSubTestOnFail : false, // user-provided config values config : null }, methods : { initialize : function () { // backward compat - both `project` and `harness` attributes should be supported this.harness = this.project = this.project || this.harness // suppress bubblings of some events (JooseX.Observable does not provide better mechanism for that, yet) this.on('teststart', function (event) { if (this.parent) event.stopPropagation() }) this.on('testfinalize', function (event) { if (this.parent) event.stopPropagation() }) this.on('teststop', function (event) { if (this.parent) event.stopPropagation() }) this.on('beforetestfinalize', function (event) { if (this.parent) event.stopPropagation() }) this.on('beforetestfinalizeearly', function (event) { if (this.parent) event.stopPropagation() }) this.subTestTimeout = this.subTestTimeout || 2 * this.defaultTimeout if (this.snoozeUntil) { this.snoozeUntil = new Date(this.snoozeUntil) if (isNaN(this.snoozeUntil - 0 )) this.snoozeUntil = null } if (this.snoozeUntil && new Date() < this.snoozeUntil) this.isTodo = true // Potentially may overwrite default properties and break test instance, should be used with care if (this.config) Joose.O.extend(this, this.config) }, /** * This method allows you to delay the start of the test, for example for performing some asynchronous setup code (like login into an application). * Note, that you may want to use the {@link #setup} method instead, as it is a bit simpler to implement. * * It is supposed to be overridden in a subclass of the Siesta.Test class and should return an object with two properties: "ready" and "reason" * ("reason" is only meaningful for the case where "ready : false"). The Test instance will poll this method and will only launch * the test after this method returns "ready : true". If waiting for this condition takes longer than {@link #isReadyTimeout}, the test * will be launched anyway, but a failing assertion will be added to it. * * **Important** This method should always check the value returned by a `this.SUPER` call. * * A typical example of using this method can be seen below: * Class('My.Test.Class', { isa : Siesta.Test.Browser, has : { isCustomSetupDone : false }, override : { isReady : function () { var result = this.SUPERARG(arguments); if (!result.ready) return result; if (!this.isCustomSetupDone) return { ready : false, reason : "Waiting for `isCustomSetupDone` took too long - something wrong?" } return { ready : true } }, start : function () { var me = this; Ext.Ajax.request({ url : 'do_login.php', params : { ... }, success : function () { me.isCustomSetupDone = true } }) this.SUPERARG(arguments) } }, .... }) * * @return {Object} Object with properties `{ ready : true/false, reason : 'description' }` */ isReady: function() { var R = Siesta.Resource('Siesta.Test'); // this should allow us to wait until the presense of "run" function // it will become available after call to StartTest method // which some users may call asynchronously, after some delay // see https://www.assembla.com/spaces/bryntum/tickets/379 // in this case test can not be configured using object as 1st argument for StartTest this.run = this.run || this.getStartTestAnchor().args && this.getStartTestAnchor().args[ 0 ] return { ready : this.typeOf(this.run) == 'Function' || this.typeOf(this.run) == 'AsyncFunction', reason : R.get('noCodeProvidedToTest') } }, // indicates that the tests are identical or from the same tree (one is parent for another) isFromTheSameGeneration : function (test2) { return this.generation == test2.generation }, toString : function() { return this.url }, // deprecated plan : function (value) { if (this.assertPlanned != null) throw new Error("Test plan can't be changed") this.assertPlanned = value }, addResult : function (result) { var isAssertion = result instanceof Siesta.Result.Assertion if (isAssertion) result.isTodo = this.isTodo // only allow to add diagnostic results and todo results after the end of test // and only if "needDone" is enabled if (isAssertion && (this.isDone || this.isFinished()) && !result.isTodo) { if (!this.testEndReported) { this.testEndReported = true var R = Siesta.Resource('Siesta.Test'); this.fail(R.get('addingAssertionsAfterDone')) } } if (isAssertion && !result.index) { result.index = ++this.assertCount } this.getResults().push(result) // clear the cache this.$passCount = this.$failCount = null /** * This event is fired when an individual test case receives a new result (assertion or diagnostic message). * * This event bubbles up to the {@link Siesta.Project project}, so you can observe it on the project as well. * * @event testupdate * @member Siesta.Test * @param {JooseX.Observable.Event} event The event instance * @param {Siesta.Test} test The test instance that just has started * @param {Siesta.Result} result The new result. Instance of Siesta.Result.Assertion or Siesta.Result.Diagnostic classes */ this.fireEvent('testupdate', this, result, this.getResults()) this.lastActivityDate = new Date(); return result }, /** * This method output the diagnostic message. * @param {String} desc The text of diagnostic message */ diag : function (desc, callback) { this.addResult(new Siesta.Result.Diagnostic({ // protection from user passing some arbitrary JSON object instead of string // (which can be circular and then test report will fail with "Converting circular structure to JSON" description : String(desc || '') })) callback && callback(); }, /** * This method add the passed assertion to this test. * * @param {String} desc The description of the assertion * @param {String/Object} [annotation] The string with additional description how exactly this assertion passes. Will be shown with monospace font. * Can be also an object with the following properties: * @param {String} annotation.annotation The actual annotation text * @param {String} annotation.descTpl The template for the default description text. Will be used if user did not provide any description for * assertion. Template can contain variables in braces. The values for variables are taken as properties of `annotation` parameters with the same name: * this.pass(desc, { descTpl : '{value1} sounds like {value2}', value1 : '1', value2 : 'one }) * */ pass : function (desc, annotation, result) { if (annotation && this.typeOf(annotation) != 'String') { // create a default assertion description if (!desc && annotation.descTpl) desc = this.formatString(annotation.descTpl, annotation) // actual annotation annotation = annotation.annotation } if (result) { result.passed = true result.description = String(desc || '') result.annotation = annotation } this.addResult(result || new Siesta.Result.Assertion({ passed : true, // protection from user passing some arbitrary JSON object instead of string // (which can be circular and then test report will fail with "Converting circular structure to JSON" annotation : String(annotation || ''), description : String(desc || ''), sourceLine : (result && result.sourceLine) || (annotation && annotation.sourceLine) || this.sourceLineForAllAssertions && this.getSourceLine() || null })) }, /** * This method add the failed assertion to this test. * * @param {String} desc The description of the assertion * @param {String/Object} annotation The additional description how exactly this assertion fails. Will be shown with monospace font. * * Can be either string or an object with the following properties. In the latter case a string will be constructed from the properties of the object. * * - `assertionName` - the name of assertion, will be shown in the 1st line, along with originating source line (in FF and Chrome only) * - `got` - an arbitrary JavaScript object, when provided will be shown on the next line * - `need` - an arbitrary JavaScript object, when provided will be shown on the next line * - `gotDesc` - a prompt for "got", default value is "Got", but can be for example: "We have" * - `needDesc` - a prompt for "need", default value is "Need", but can be for example: "We need" * - `annotation` - A text to append on the last line, can contain some additional explanations * * The "got" and "need" values will be stringified to the "not quite JSON" notation. Notably the points of circular references will be * marked with `[Circular]` marks and the values at 4th (and following) level of depth will be marked with triple points: `[ [ [ ... ] ] ]` */ fail : function (desc, annotation, result) { var sourceLine = (result && result.sourceLine) || (annotation && annotation.sourceLine) || this.getSourceLine() var assertionName = ''; if (annotation && this.typeOf(annotation) != 'String') { if (!desc && annotation.descTpl) desc = this.formatString(annotation.descTpl, annotation) var strings = [] var params = annotation var hasGot = params.hasOwnProperty('got') var hasNeed = params.hasOwnProperty('need') var gotDesc = params.gotDesc || 'Got' var needDesc = params.needDesc || 'Need' assertionName = params.assertionName annotation = params.annotation if (!params.ownTextOnly && (assertionName || sourceLine)) strings.push( 'Failed assertion ' + (assertionName ? '`' + assertionName + '` ' : '') + this.formatSourceLine(sourceLine) ) if (hasGot && hasNeed) { var max = Math.max(gotDesc.length, needDesc.length) gotDesc = this.appendSpaces(gotDesc, max - gotDesc.length + 1) needDesc = this.appendSpaces(needDesc, max - needDesc.length + 1) } if (hasGot) strings.push(gotDesc + ': ' + Siesta.Util.Serializer.stringify(params.got)) if (hasNeed) strings.push(needDesc + ': ' + Siesta.Util.Serializer.stringify(params.need)) if (annotation) strings.push(annotation) annotation = strings.join('\n') } if (result) { // Failing a pending waitFor operation result.name = assertionName; result.passed = false; result.annotation = annotation; result.description = desc; } this.addResult(result || new Siesta.Result.Assertion({ name : assertionName, passed : false, sourceLine : sourceLine, // protection from user passing some arbitrary JSON object instead of string // (which can be circular and then test report will fail with "Converting circular structure to JSON" annotation : String(annotation || ''), description : String(desc || '') })) this.onFailedAssertion() }, onFailedAssertion : function (noNeedToExit) { if (!this.isTodo) { if (this.project.debuggerOnFail) eval("debugger") if (this.project.breakOnFail && !this.__STOPPED__) { this.__STOPPED__ = true this.project.stopCurrentLaunch(this) if (!noNeedToExit) this.exit() } if ((this.breakTestOnFail || this.breakSubTestOnFail) && !this.__STOPPED__) { this.__STOPPED__ = true if (!noNeedToExit) { if (this.breakTestOnFail) { this.getRootTest().exit() } else { this.exit() } } } } }, /** * This method interrupts the test execution. You can use it if, for example, you already know the status of * test (failed) and further actions involves long waitings etc. * * This method accepts the same arguments as the {@link #fail} method. If at least the one argument is given, * a failed assertion will be added to the test before the exit. * * The interruption is performed by throwing an exception from the test. If you have the * {@link Siesta.Project#transparentEx transparentEx} option enabled you will observe it in the debugger/console. * * See also {@link Siesta.Project#breakTestOnFail breakTestOnFail}, {@link Siesta.Project#breakSubTestOnFail breakSubTestOnFail} * * For example: * t.chain( function (next) { // do something next() }, function (next) { if (someCondition) t.exit("Failure description") else next() }, { waitFor : function () { ... } } ) * * @param {String} [desc] The description of the assertion * @param {String/Object} [annotation] The additional description how exactly this assertion fails. Will be shown with monospace font. */ exit : function (desc, annotation) { if (arguments.length > 0) this.fail(desc, annotation) this.finalize(true) throw '__SIESTA_TEST_EXIT_EXCEPTION__' }, getSource : function () { return this.contentManager.getContentOf(this.url) }, getSourceLine : function () { var stack = new Error().stack if (!stack) { try { throw new Error() } catch (e) { stack = e.stack } } if (stack) { var match = stack.match(this.urlExtractRegex()) if (match) return match[ 1 ] } return null }, getStartTestAnchor : function () { return this.startTestAnchor }, getExceptionCatcher : function () { return this.exceptionCatcher }, getTestErrorClass : function () { return this.testErrorClass }, processCallbackFromTest : function (callback, args, scope) { var me = this if (!callback) return true; if (this.transparentEx) { callback.apply(scope || this.global, args || []) } else { var e = this.getExceptionCatcher()(function () { callback.apply(scope || me.global, args || []) }) if (e) { this.failWithException(e) // flow should be interrupted - exception detected return false } } // flow can be continued return true }, getStackTrace : function (e) { if (Object(e) !== e) return null if (!e.stack) return null var stackLines = (e.stack + '').split('\n') var message = e + '' var R = Siesta.Resource('Siesta.Test'); var result = [] var match for (var i = 0; i < stackLines.length; i++) { var line = stackLines[ i ] if (!line) continue // first line should contain exception message if (!i) { if (line != message) result.push(message) else { result.push(line) continue; } } result.push(line) } if (!result.length) return null return result }, formatSourceLine : function (sourceLine) { var R = Siesta.Resource('Siesta.Test'); return sourceLine ? (R.get('atLine') + ' ' + sourceLine + ' ' + R.get('of') + ' ' + this.url) : '' }, appendSpaces : function (str, num) { var spaces = '' while (num--) spaces += ' ' return str + spaces }, eachAssertion : function (func, scope) { scope = scope || this this.getResults().each(function (result) { if (result instanceof Siesta.Result.Assertion) func.call(scope, result) }) }, eachSubTest : function (func, scope) { scope = scope || this this.getResults().each(function (result) { if (result instanceof Siesta.Result.SubTest) if (func.call(scope, result.test) === false) return false }) }, eachChildTest : function (func, scope) { scope = scope || this this.getResults().eachChild(function (result) { if (result instanceof Siesta.Result.SubTest) if (func.call(scope, result.test) === false) return false }) }, /** * This assertion passes when the supplied `value` evalutes to `true` and fails otherwise. * * @param {Mixed} value The value, indicating wheter assertions passes or fails * @param {String} [desc] The description of the assertion */ ok : function (value, desc) { var R = Siesta.Resource('Siesta.Test'); if (value) this.pass(desc, { descTpl : R.get('isTruthy'), value : value }) else this.fail(desc, { assertionName : 'ok', got : value, annotation : R.get('needTruthy') }) }, notok : function () { this.notOk.apply(this, arguments) }, /** * This assertion passes when the supplied `value` evalutes to `false` and fails otherwise. * * It has a synonym - `notok`. * * @param {Mixed} value The value, indicating wheter assertions passes or fails * @param {String} [desc] The description of the assertion */ notOk : function (value, desc) { var R = Siesta.Resource('Siesta.Test'); if (!value) this.pass(desc, { descTpl : R.get('isFalsy'), value : value }) else this.fail(desc, { assertionName : 'notOk', got : value, annotation : R.get('needFalsy') }) }, /** * This assertion passes when the comparison of 1st and 2nd arguments with `==` operator returns true and fails otherwise. * * As a special case, one or both arguments can be *placeholders*, generated with method {@link #any}. * * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure * @param {String} [desc] The description of the assertion */ is : function (got, expected, desc) { var R = Siesta.Resource('Siesta.Test'); if (expected && got instanceof this.global.Date) { this.isDateEqual(got, expected, desc); } else if (this.compareObjects(got, expected, false, true)) this.pass(desc, { descTpl : R.get('isEqualTo'), got : got, expected : expected }) else this.fail(desc, { assertionName : 'is', got : got, need : expected }) }, isnot : function () { this.isNot.apply(this, arguments) }, isnt : function () { this.isNot.apply(this, arguments) }, /** * This assertion passes when the comparison of 1st and 2nd arguments with `!=` operator returns true and fails otherwise. * It has synonyms - `isnot` and `isnt`. * * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}. * * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure * @param {String} [desc] The description of the assertion */ isNot : function (got, expected, desc) { var R = Siesta.Resource('Siesta.Test'); if (!this.compareObjects(got, expected, false, true)) this.pass(desc, { descTpl : R.get('isNotEqualTo'), got : got, expected : expected }) else this.fail(desc, { assertionName : 'isnt', got : got, need : expected, needDesc : R.get('needNot') }) }, /** * This assertion passes when the comparison of 1st and 2nd arguments with `===` operator returns true and fails otherwise. * * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}. * * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure * @param {String} [desc] The description of the assertion */ exact : function (got, expected, desc) { var R = Siesta.Resource('Siesta.Test'); if (this.compareObjects(got, expected, true, true)) this.pass(desc, { descTpl : R.get('isStrictlyEqual'), got : got, expected : expected }) else this.fail(desc, { assertionName : 'isStrict', got : got, need : expected, needDesc : R.get('needStrictly') }) }, /** * This assertion passes when the comparison of 1st and 2nd arguments with `===` operator returns true and fails otherwise. * * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}. * * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure * @param {String} [desc] The description of the assertion */ isStrict : function (got, expected, desc) { var R = Siesta.Resource('Siesta.Test'); if (this.compareObjects(got, expected, true, true)) this.pass(desc, { descTpl : R.get('isStrictlyEqual'), got : got, expected : expected }) else this.fail(desc, { assertionName : 'isStrict', got : got, need : expected, needDesc : R.get('needStrictly') }) }, isntStrict : function () { this.isNotStrict.apply(this, arguments) }, /** * This assertion passes when the comparison of 1st and 2nd arguments with `!==` operator returns true and fails otherwise. * It has synonyms - `isntStrict`. * * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}. * * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure * @param {String} [desc] The description of the assertion */ isNotStrict : function (got, expected, desc) { var R = Siesta.Resource('Siesta.Test'); if (!this.compareObjects(got, expected, true, true)) this.pass(desc, { descTpl : R.get('isStrictlyNotEqual'), got : got, expected : expected }) else this.fail(desc, { assertionName : 'isntStrict', got : got, need : expected, needDesc : R.get('needStrictlyNot') }) }, // DEPRECATED in 5.0 wait : function (title, howLong) { var R = Siesta.Resource('Siesta.Test'); if (this.waitTitles.hasOwnProperty(title)) throw new Error(R.get('alreadyWaiting')+ " [" + title + "]") return this.waitTitles[ title ] = this.beginAsync(howLong) }, // DEPRECATED in 5.0 endWait : function (title) { var R = Siesta.Resource('Siesta.Test'); if (!this.waitTitles.hasOwnProperty(title)) throw new Error(R.get('noOngoingWait') + " [" + title + "]") this.endAsync(this.waitTitles[ title ]) delete this.waitTitles[ title ] }, /** * This method starts the "asynchronous frame". The test will wait for all asynchronous frames to complete before it will finalize. * The frame should be finished with the {@link #endAsync} call within the provided `time`, otherwise a failure will be reported. * * For example: * * var async = t.beginAsync() * * Ext.require('Some.Class', function () { * * t.ok(Some.Class, 'Some class was loaded') * * t.endAsync(async) * }) * * * Additionally, if you return a `Promise` instance from the test function itself, Siesta will wait until that promise is resolved before finalizing the test. * In modern browsers, this allows us to use `async/await` functions: StartTest(t => { let someAsyncOperation = () => new Promise((resolve, reject) => { setTimeout(() => resolve(true), 1000) }) t.it('Doing async stuff', async t => { let res = await someAsyncOperation() t.ok(res, "Async stuff finished correctly") }) }) * @param {Number} time The maximum time (in ms) to wait until force the finalization of this async frame. Optional. Default time is 15000 ms. * @param {Function} errback Optional. The function to call in case the call to {@link #endAsync} was not detected withing `time`. If function * will return any "truthy" value, the failure will not be reported (you can report own failure with this errback). * * @return {Object} The frame object, which can be used in {@link #endAsync} call */ beginAsync : function (time, errback) { time = time || this.defaultTimeout if (time > this.getMaximalTimeout()) this.fireEvent('maxtimeoutchanged', time) var R = Siesta.Resource('Siesta.Test'); var me = this var originalSetTimeout = this.originalSetTimeout var index = this.timeoutsCount.counter++ // in NodeJS `setTimeout` returns an object and not a simple ID, so we try hard to store that object under unique index // also using `setTimeout` from the scope of test - as timeouts in different scopes in browsers are mis-synchronized // can't just use `this.originalSetTimeout` because of scoping issues var timeoutId = originalSetTimeout(function () { if (me.hasAsyncFrame(index)) { if (!errback || !errback.call(me, me)) me.fail(R.get('noMatchingEndAsync', { time : time })) me.endAsync(index) } }, time) this.timeoutIds[ index ] = timeoutId return index }, timeoutIdToIndex : function (id) { var index if (typeof id == 'object') { index = id.__index } else { index = this.idsToIndex[ id ] } return index }, hasAsyncFrame : function (index) { return this.timeoutIds.hasOwnProperty(index) }, hasAsyncFrameByTimeoutId : function (id) { return this.timeoutIds.hasOwnProperty(this.timeoutIdToIndex(id)) }, /** * This method finalize the "asynchronous frame" started with {@link #beginAsync}. * * @param {Object} frame The frame to finalize (returned by {@link #beginAsync} method */ endAsync : function (index) { var originalSetTimeout = this.originalSetTimeout var originalClearTimeout = this.originalClearTimeout || this.global.clearTimeout var counter = 0 var R = Siesta.Resource('Siesta.Test'); if (index == null) Joose.O.each(this.timeoutIds, function (timeoutId, indx) { index = indx if (counter++) throw new Error(R.get('endAsyncMisuse')) }) var timeoutId = this.timeoutIds[ index ] // need to call in this way for IE < 9 originalClearTimeout(timeoutId) delete this.timeoutIds[ index ] var me = this if (this.processed && !this.isFinished()) // to allow potential call to `done` after `endAsync` originalSetTimeout(function () { me.finalize() }, 1) }, clearTimeouts : function () { var originalClearTimeout = this.originalClearTimeout Joose.O.each(this.timeoutIds, function (value, id) { originalClearTimeout(value) }) this.timeoutIds = {} }, processSubTestConfig : function (config) { var cfg = Joose.O.extend({ parent : this, isTodo : this.isTodo, transparentEx : this.transparentEx, waitForTimeout : this.waitForTimeout, waitForPollInterval : this.waitForPollInterval, defaultTimeout : this.defaultTimeout, timeout : this.subTestTimeout, subTestTimeout : this.subTestTimeout, global : this.global, url : this.url, scopeProvider : this.scopeProvider, project : this.project, generation : this.generation, launchId : this.launchId, overrideSetTimeout : this.overrideSetTimeout, originalSetTimeout : this.originalSetTimeout, originalClearTimeout : this.originalClearTimeout, // share the same counter for the whole subtests tree timeoutsCount : this.timeoutsCount, autoCheckGlobals : false, needToCleanup : false, breakTestOnFail : this.breakTestOnFail, breakSubTestOnFail : this.breakSubTestOnFail }, config) return cfg }, /** * Returns a new instance of the test class, configured as being a "sub test" of the current test. * * The number of nesting levels is not limited - ie sub-tests may have own sub-tests. * * Note, that this method does not starts the sub test, but only instatiate it. To start the sub test, * use the {@link #launchSubTest} method or the {@link #subTest} helper method. * * @param {String} name The name of the test. Will be used in the UI, as the parent node name in the assertions tree * @param {Function} code A function with test code. Will receive a test instance as the 1st argument. * @param {Number} [timeout] A maximum duration (in ms) for this sub test. If test will not complete within this time, * it will be considered failed. If not provided, the {@link Siesta.Project#subTestTimeout} value is used. * * @return {Siesta.Test} A sub test instance */ getSubTest : function (arg1, arg2, arg3) { var config var R = Siesta.Resource('Siesta.Test'); if (arguments.length == 2 || arguments.length == 3) config = { name : arg1, run : arg2, timeout : arg3 } else if (arguments.length == 1 && this.typeOf(arg1) == 'Function') config = { name : 'Sub test', run : arg1 } config = config || arg1 || {} // pass-through only valid timeout values if (config.timeout == null) delete config.timeout var name = config.name if (!config.run) { this.failWithException(R.get('codeBodyMissingForSubTest', { name : name })) throw new Error(R.get('codeBodyMissingForSubTest', { name : name })) } if (!config.run.length) { this.failWithException(R.get('codeBodyMissingTestArg', { name : name })) throw new Error(R.get('codeBodyMissingTestArg', { name : name })) } // in the following code, we cache a specialized version of the current test class, // that version has "Siesta.Test.Sub" role consumed, as a trait (on the instance level) // this is done for efficiency (to not create a separate class per every sub test) // and also because Chrome was throwing this exception randomly: // Required attribute [parent] is missed during initialization of undefined // caching seems to have that fixed var constructor = config.meta || this.meta.c var cls = constructor var cfg = this.processSubTestConfig(config) if (constructor.__WITHSUB__) cls = constructor.__WITHSUB__ else // only need trait for the top level test if (!this.parent) cfg.trait = Siesta.Test.Sub var subTest = new cls(cfg) if (!this.parent && !constructor.__WITHSUB__) constructor.__WITHSUB__ = subTest.meta.c return subTest }, /** * This method launch the provided sub test instance. * * @param {Siesta.Test} subTest A test instance to launch * @param {Function} callback A function to call, after the test is completed. This function is called regardless from the test execution result. */ launchSubTest : function (subTest, callback) { if (this.parent && this.parent.finalizationStarted) return var me = this var R = Siesta.Resource('Siesta.Test'); var timeout = subTest.timeout || this.subTestTimeout var async = this.beginAsync(timeout, function () { me.fail(R.get('failedToFinishWithin', { name : subTest.name ? '[' + subTest.name + ']' : '', timeout : timeout })) me.restoreTimeoutOverrides() testEndListener.remove() subTest.finalize(true) callback && callback(subTest) return true }) var testEndListener = subTest.on('testfinalize', function () { me.endAsync(async) me.restoreTimeoutOverrides() callback && callback(subTest) }) this.addResult(subTest.getResults()) subTest.start() }, /** * With this method you can mark a group of assertions as "todo", assuming they most likely will fail, * but it's still worth to try to run them. * The supplied `code` function will be run, it will receive a new test instance as the 1st argument, * which should be used for assertion checks (and not the primary test instance, received from `StartTest`). * * Assertions, failed inside of the `code` block will be still treated by project as "green". * Assertions, passed inside of the `code` block will be treated by project as bonus ones and highlighted. * * See also {@link Siesta.Test.ExtJS#knownBugIn} and {@link Siesta.Test.ExtJS#snooze} methods. Note, that this method will start a new {@link #subTest sub test}. * * For example: t.todo('Scheduled for 4.1.x release', function (todo) { var treePanel = new Ext.tree.Panel() todo.is(treePanel.getView().store, treePanel.store, 'NodeStore and TreeStore have been merged and there is only 1 store now'); }) * @param {String} why The reason/description for the todo * @param {Function} code A function, wrapping the "todo" assertions. This function will receive a special test class instance * which should be used for assertion checks */ todo : function (why, code, callback) { if (this.typeOf(why) == 'Function') why = [ code, code = why ][ 0 ] var todo = this.getSubTest({ name : why, run : code, isTodo : true, transparentEx : false }) this.launchSubTest(todo, callback) }, /** * This method allows you to "snooze" the failing test (make it a {@link Siesta.Test#todo todo test} until certain date. * After that date, test will become "normal" again. Use with care :) * t.snooze('2014-10-10', function (todo) { var treePanel = new Ext.tree.Panel() todo.is(treePanel.getView().store, treePanel.store, 'NodeStore and TreeStore have been merged and there is only 1 store now'); }) * * @param {String/Date} snoozeUntilDate The date until which we don't want to hear about this test. Can be provided as `Date` instance or a string, recognized by `Date` constructor * @param {Function} fn The function body of the test, will receive a new test instance as 1st argument * @param {String} reason The reason or explanation why this test is "snoozed" */ snooze : function(snoozeUntilDate, fn, reason) { var R = Siesta.Resource('Siesta.Test'); var snoozeDate = new Date(snoozeUntilDate) if (new Date() > snoozeDate) { this.it('Unsnoozed', fn, null, false, false); } else { this.it(R.get('Snoozed until') + ' ' + snoozeDate + ': ' + (reason || ''), fn, null, false, true); } }, /** * T