siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
1,278 lines (975 loc) • 94.2 kB
HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The source code</title>
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
<style type="text/css">
.highlight { display: block; background-color: #ddd; }
</style>
<script type="text/javascript">
function highlight() {
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
}
</script>
</head>
<body onload="prettyPrint(); highlight();">
<pre class="prettyprint lang-js">/*
Siesta 5.6.1
Copyright(c) 2009-2022 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license
*/
<span id='Siesta-Test'>/**
</span>@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__
},
<span id='Siesta-Test-property-url'> /**
</span> * @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,
<span id='Siesta-Test-cfg-isReadyTimeout'> /**
</span> * @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)
},
<span id='Siesta-Test-method-isReady'> /**
</span> * 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
<span id='Siesta-Test-event-testupdate'> /**
</span> * 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
},
<span id='Siesta-Test-method-diag'> /**
</span> * 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();
},
<span id='Siesta-Test-method-pass'> /**
</span> * 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
}))
},
<span id='Siesta-Test-method-fail'> /**
</span> * 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()
}
}
}
}
},
<span id='Siesta-Test-method-exit'> /**
</span> * 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
})
},
<span id='Siesta-Test-method-ok'> /**
</span> * 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)
},
<span id='Siesta-Test-method-notOk'> /**
</span> * 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')
})
},
<span id='Siesta-Test-method-is'> /**
</span> * 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)
},
<span id='Siesta-Test-method-isNot'> /**
</span> * 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')
})
},
<span id='Siesta-Test-method-exact'> /**
</span> * 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')
})
},
<span id='Siesta-Test-method-isStrict'> /**
</span> * 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)
},
<span id='Siesta-Test-method-isNotStrict'> /**
</span> * 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 ]
},
<span id='Siesta-Test-method-beginAsync'> /**
</span> * 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))
},
<span id='Siesta-Test-method-endAsync'> /**
</span> * 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
},
<span id='Siesta-Test-method-getSubTest'> /**
</span> * 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
},
<span id='Siesta-Test-method-launchSubTest'> /**
</span> * 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(subT