siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
709 lines (556 loc) • 26.7 kB
JavaScript
/*
Siesta 5.6.1
Copyright(c) 2009-2022 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license
*/
/**
@class Siesta.Test.BDD
A mixin providing a BDD style layer for most of the assertion methods.
It is consumed by {@link Siesta.Test}, so all of its methods are available in all tests.
*/
Role('Siesta.Test.BDD', {
requires : [
'getSubTest', 'chain'
],
has : {
specType : null, // `describe` or `it`
beforeEachHooks : Joose.I.Array,
afterEachHooks : Joose.I.Array,
sequentialSubTests : Joose.I.Array,
// flag, whether the "run" function of the test (containing actual test code) have been already run
codeProcessed : false,
launchTimeout : null,
// Siesta.Test.BDD.Expectation should already present on the page
expectationClass : Siesta.Test.BDD.Expectation,
failOnExclusiveSpecsWhenAutomated : false,
spies : Joose.I.Array
},
methods : {
checkSpecFunction : function (func, type, name) {
if (!func) throw new Error(Siesta.Resource('Siesta.Test.BDD', 'codeBodyMissing') + " " + (type == 'describe' ? 'suite' : 'spec') + ' [' + name + ']')
if (!func.length) throw new Error(Siesta.Resource('Siesta.Test.BDD', 'codeBodyOf') + " " + (type == 'describe' ? 'suite' : 'spec') + ' [' + name + '] ' + Siesta.Resource('Siesta.Test.BDD', 'missingFirstArg'))
},
/**
* This is an "exclusive" version of the regular {@link #describe} suite. When such suites presents in some test file,
* the other regular suites at the same level will not be executed, only "exclusive" ones.
*
* @param {String} name The name or description of the suite
* @param {Function} code The code function for this suite. It will receive a test instance as the first argument which should be used for all assertion methods.
* @param {Number} [timeout] A maximum duration for this suite. If not provided {@link Siesta.Project#subTestTimeout} value is used.
*/
ddescribe : function (name, code, timeout) {
this.describe(name, code, timeout, true)
},
/**
* This is a no-op method, allowing you to quickly ignore some suites.
*/
xdescribe : function () {
},
/**
* This method starts a sub test with *suite* (in BDD terms). Such suite consists from one or more *specs* (see method {@link #it}} or other suites.
* The number of nesting levels is not limited. All suites of the same nesting level are executed sequentially.
*
* For example:
*
t.describe('A product', function (t) {
t.it('should have feature X', function (t) {
...
})
t.describe('feature X', function (t) {
t.it('should be cool', function (t) {
...
})
})
})
*
* See also {@link #beforeEach}, {@link #afterEach}, {@link #xdescribe}, {@link #ddescribe}
*
* @param {String} name The name or description of the suite
* @param {Function} code The code function for this suite. It will receive a test instance as the first argument which should be used for all assertion methods.
* @param {Number} [timeout] A maximum duration for this suite. If not provided {@link Siesta.Project#subTestTimeout} value is used.
*/
describe : function (name, code, timeout, isExclusive) {
this.checkSpecFunction(code, 'describe', name)
var subTest = this.getSubTest({
name : name,
run : code,
isExclusive : isExclusive,
specType : 'describe',
timeout : timeout
})
if (this.codeProcessed) this.scheduleSpecsLaunch()
this.sequentialSubTests.push(subTest)
},
/**
* This is an "exclusive" version of the regular {@link #it} spec. When such specs presents in some suite,
* the other regular specs at the same level will not be executed, only "exclusive" ones. Note, that even "regular" suites (`t.describe`) sections
* will be ignored, if they are on the same level with the exclusive `iit` section.
*
* @param {String} name The name or description of the spec
* @param {Function} code The code function for this spec. It will receive a test instance as the first argument which should be used for all assertion methods.
* @param {Number} [timeout] A maximum duration for this spec. If not provided {@link Siesta.Project#subTestTimeout} value is used.
*/
iit : function (name, code, timeout) {
if (this.project.isAutomated) {
if (this.failOnExclusiveSpecsWhenAutomated) this.fail(Siesta.Resource('Siesta.Test.BDD', 'iitFound'));
}
this.it(name, code, timeout, true)
},
/**
* This is a no-op method, allowing you to quickly ignore some specs.
*/
xit : function () {
},
/**
* This method starts a sub test with *spec* (in BDD terms). Such spec consists from one or more assertions (or *expectations*, *matchers*, etc) or other nested specs
* and/or suites. See the {@link #expect} method. The number of nesting levels is not limited. All specs of the same nesting level are executed sequentially.
*
* For example:
*
t.describe('A product', function (t) {
t.it('should have feature X', function (t) {
...
})
t.it('should have feature Y', function (t) {
...
})
})
*
* See also {@link #beforeEach}, {@link #afterEach}, {@link #xit}, {@link #iit}
*
* @param {String} name The name or description of the spec
* @param {Function} code The code function for this spec. It will receive a test instance as the first argument which should be used for all assertion methods.
* @param {Number} [timeout] A maximum duration for this spec. If not provided {@link Siesta.Project#subTestTimeout} value is used.
*/
it : function (name, code, timeout, isExclusive, isTodo) {
this.checkSpecFunction(code, 'it', name)
var subTest = this.getSubTest({
name : name,
run : code,
isExclusive : isExclusive,
isTodo : Boolean(isTodo) || this.isTodo,
specType : 'it',
timeout : timeout
})
if (this.codeProcessed) this.scheduleSpecsLaunch()
this.sequentialSubTests.push(subTest)
},
/**
* This method returns an "expectation" instance, which can be used to check various assertions about the passed value.
*
* **Note**, that every expectation has a special property `not`, that contains another expectation, but with the negated meaning.
*
* For example:
*
t.expect(1).toBe(1)
t.expect(1).not.toBe(2)
t.expect('Foo').toContain('oo')
t.expect('Foo').not.toContain('bar')
* Please refer to the documentation of the {@link Siesta.Test.BDD.Expectation} class for the list of available methods.
*
* @param {Mixed} value Any value, that will be assert about
* @return {Siesta.Test.BDD.Expectation} Expectation instance
*/
expect : function (value) {
return new this.expectationClass({
t : this,
value : value
})
},
/**
* This method returns a *placeholder*, denoting any instance of the provided class constructor. Such placeholder can be used in various
* comparison assertions, like {@link #is}, {@link #isDeeply}, {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
* {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
*
* For example:
t.is(1, t.any(Number))
t.expect(1).toBe(t.any(Number))
t.isDeeply({ name : 'John', age : 45 }, { name : 'John', age : t.any(Number))
t.expect({ name : 'John', age : 45 }).toEqual({ name : 'John', age : t.any(Number))
t.is(NaN, t.any(), 'When class constructor is not provided `t.any()` should match anything')
*
* See also {@link #anyNumberApprox}, {@link #anyStringLike}.
*
* @param {Function} clsConstructor A class constructor instances of which are denoted with this placeholder. As a special case if this argument
* is not provided, a placeholder will match any value.
*
* @return {Object} A placeholder object
*/
any : function (clsConstructor) {
return new Siesta.Test.BDD.Placeholder({
clsConstructor : clsConstructor,
t : this,
context : this.global
})
},
/**
* This method returns a *placeholder*, denoting any number approximately equal to the provided value.
* Such placeholder can be used in various comparison assertions, like {@link #is}, {@link #isDeeply},
* {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
* {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
*
* For example:
t.is(1, t.anyNumberApprox(1.2, 0.5))
t.expect(1).toBe(t.anyNumberApprox(1.2, 0.5))
*
* @param {Number} value The approximate value
* @param {Number} [threshold] The threshold. If omitted, it is set to 5% from the `value`.
*
* @return {Object} A placeholder object
*/
anyNumberApprox : function (value, threshold) {
return new Siesta.Test.BDD.NumberPlaceholder({
value : value,
threshold : threshold
})
},
/**
* This method returns a *placeholder*, denoting any string that matches provided value.
* Such placeholder can be used in various comparison assertions, like {@link #is}, {@link #isDeeply},
* {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
* {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
*
* For example:
t.is('foo', t.anyStringLike('oo'))
t.expect('bar').toBe(t.anyStringLike(/ar$/))
*
* @param {String/RegExp} value If given as string will denote a substring a string being checked should contain,
* if given as RegExp instance then string being checked should match this RegExp
*
* @return {Object} A placeholder object
*/
anyStringLike : function (value) {
return new Siesta.Test.BDD.StringPlaceholder({ value : value })
},
scheduleSpecsLaunch : function () {
if (this.launchTimeout) return
var async = this.beginAsync()
var originalSetTimeout = this.originalSetTimeout
var me = this
this.launchTimeout = originalSetTimeout(function () {
me.endAsync(async)
me.launchTimeout = null
me.launchSpecs()
}, 0)
},
runBeforeSpecHooks : function (sourceTest, done) {
var me = this
var runOwnHooks = function (done) {
me.chainForArray(me.beforeEachHooks, function (hook) {
return function (next) {
var code = hook.code
if (me.typeOf(code) === 'AsyncFunction') {
return code(sourceTest, function () {})
} else {
if (hook.isAsync) {
code(sourceTest, next)
} else {
code(sourceTest)
next()
}
}
}
}, done)
}
if (this.parent)
this.parent.runBeforeSpecHooks(sourceTest, function () {
runOwnHooks(done)
})
else
runOwnHooks(done)
},
runAfterSpecHooks : function (sourceTest, done) {
var me = this
me.chainForArray(
this.afterEachHooks, function (hook) {
return function (next) {
var code = hook.code
if (me.typeOf(code) === 'AsyncFunction') {
return code(sourceTest, function () {})
} else {
if (hook.isAsync) {
code(sourceTest, next)
} else {
code(sourceTest)
next()
}
}
}
}, function () {
me.parent ? me.parent.runAfterSpecHooks(sourceTest, done) : done()
},
// reverse
true
)
},
launchSpecs : function () {
var me = this
var sequentialSubTests = this.sequentialSubTests
this.sequentialSubTests = []
// hackish way to pass a config to `t.chain`
this.chain.actionDelay = 0
var exclusiveSubTests = []
Joose.A.each(sequentialSubTests, function (subTest) {
if (subTest.isExclusive) exclusiveSubTests.push(subTest)
})
this.chainForArray(exclusiveSubTests.length ? exclusiveSubTests : sequentialSubTests, function (subTest) {
return [
subTest.specType == 'it' ? function (next) {
if (me.finalizationStarted || me.endDate)
next()
else
me.runBeforeSpecHooks(subTest, next)
} : null,
subTest,
subTest.specType == 'it' ? function (next) {
if (me.finalizationStarted || me.endDate)
next()
else
me.runAfterSpecHooks(subTest, next)
} : null
]
})
},
/**
* This method allows you to execute some "setup" code hook before every spec ("it" block) of the current test.
* Such hooks are **not** executed for the "describe" blocks and sub-tests generated with
* the {@link Siesta.Test#getSubTest getSubTest} method.
*
* Note, that specs can be nested and all `beforeEach` hooks are executed in order, starting from the outer-most one.
*
* The 1st argument of the hook function is always the test instance being launched.
*
* If the hook function is async (`async () => {}`) Siesta will "await" until it completes.
*
* If hook is declared with 2 arguments - it is supposed to be asynchronous (you can also force the asynchronous
* mode with the `isAsync` argument, see below). The completion callback will be provided as the 2nd argument for the hook.
*
* This method can be called several times, providing several "hook" functions.
*
* For example:
StartTest(function (t) {
var baz = 0
t.beforeEach(function (t) {
// the `t` instance here is the "t" instance from the "it" block below
baz = 0
})
t.it("This feature should work", function (t) {
t.expect(myFunction(baz++)).toEqual('someResult')
})
})
*
* @param {Function} code A function to execute before every spec
* @param {Siesta.Test} code.t A test instance being launched
* @param {Function} code.next A callback to call when the `beforeEach` method completes. This argument is only provided
* when hook function is declared with 2 arguments (or the `isAsync` argument is passed as `true`)
* @param {Boolean} isAsync When passed as `true` this argument makes the `beforeEach` method asynchronous. In this case,
* the `code` function will receive an additional callback argument, which should be called once the method has completed its work.
*
* Note, that `beforeEach` method should complete within {@link Siesta.Test#defaultTimeout defaultTimeout} time, otherwise
* failing assertion will be added to the test.
*
* Example of asynchronous hook:
StartTest(function (t) {
var baz = 0
var delay = (time) => new Promise(resolve => setTimeout(resolve, time))
// asynchronous hook function
t.beforeEach(async t => {
await delay(100)
baz = 0
})
// asynchronous setup code
t.beforeEach(function (t, next) {
// `beforeEach` will complete in 100ms
setTimeout(function () {
baz = 0
next()
}, 100)
})
t.describe("This feature should work", function (t) {
t.expect(myFunction(baz++)).toEqual('someResult')
})
})
*/
beforeEach : function (code, isAsync) {
this.beforeEachHooks.push({ code : code, isAsync : isAsync || code.length == 2 })
},
/**
* This method allows you to execute some "setup" code hook after every spec ("it" block) of the current test.
* Such hooks are **not** executed for the "describe" blocks and sub-tests generated with
* the {@link Siesta.Test#getSubTest getSubTest} method.
*
* Note, that specs can be nested and all `afterEach` hooks are executed in order, starting from the most-nested one.
*
* The 1st argument of the hook function is always the test instance being launched.
*
* If the hook function is async (`async () => {}`) Siesta will "await" until it completes.
*
* If hook is declared with 2 arguments - it is supposed to be asynchronous (you can also force the asynchronous
* mode with the `isAsync` argument, see below). The completion callback will be provided as the 2nd argument for the hook.
*
* This method can be called several times, providing several "hook" functions.
*
* For example:
StartTest(function (t) {
var baz = 0
t.afterEach(function (t) {
// the `t` instance here is the "t" instance from the "it" block below
baz = 0
})
t.it("This feature should work", function (t) {
t.expect(myFunction(baz++)).toEqual('someResult')
})
})
*
* @param {Function} code A function to execute after every spec
* @param {Siesta.Test} code.t A test instance being completed
* @param {Function} code.next A callback to call when the `afterEach` method completes. This argument is only provided
* when hook function is declared with 2 arguments (or the `isAsync` argument is passed as `true`)
* @param {Boolean} isAsync When passed as `true` this argument makes the `afterEach` method asynchronous. In this case,
* the `code` function will receive an additional callback argument, which should be called once the method has completed its work.
*
* Note, that `afterEach` method should complete within {@link Siesta.Test#defaultTimeout defaultTimeout} time, otherwise
* failing assertion will be added to the test.
*
* Example of asynchronous hook:
StartTest(function (t) {
var baz = 0
var delay = (time) => new Promise(resolve => setTimeout(resolve, time))
// asynchronous hook function
t.beforeEach(async t => {
await delay(100)
baz = 0
})
// asynchronous setup code
t.afterEach(function (t, next) {
// `afterEach` will complete in 100ms
setTimeout(function () {
baz = 0
next()
}, 100)
})
t.describe("This feature should work", function (t) {
t.expect(myFunction(baz++)).toEqual('someResult')
})
})
*/
afterEach : function (code, isAsync) {
this.afterEachHooks.push({ code : code, isAsync : isAsync || code.length == 2 })
},
/**
* This method installs a "spy" instead of normal function in some object. The "spy" is basically another function,
* which tracks the calls to itself. With spies, one can verify that some function was called and that
* it was called with certain arguments.
*
* By default, spy will call the original method and return a value from it. To enable different behavior, you can use one of these methods:
*
* - {@link Siesta.Test.BDD.Spy#returnValue returnValue} - return a specific value
* - {@link Siesta.Test.BDD.Spy#callThrough callThrough} - call the original method and return a value from it
* - {@link Siesta.Test.BDD.Spy#stub stub} - call the original method and return a value from it
* - {@link Siesta.Test.BDD.Spy#callFake callFake} - call the provided function and return a value from it
* - {@link Siesta.Test.BDD.Spy#throwError throwError} - throw a specific exception object
*
const spy = t.spyOn(obj, 'process')
// or, if you need to call some method instead
const spy = t.spyOn(obj, 'process').and.callFake(() => {
// is called instead of `process` method
})
// call the method
obj.process('fast', 1)
t.expect(spy).toHaveBeenCalled();
t.expect(spy).toHaveBeenCalledWith('fast', 1);
*
* See also {@link #createSpy}, {@link #createSpyObj}, {@link Siesta.Test.BDD.Expectation#toHaveBeenCalled toHaveBeenCalled},
* {@link Siesta.Test.BDD.Expectation#toHaveBeenCalledWith toHaveBeenCalledWith}
*
* See also the {@link Siesta.Test.BDD.Spy} class for additional details.
*
* @param {Object} object An object which property is being spied
* @param {String} propertyName A name of the property over which to install the spy.
*
* @return {Siesta.Test.BDD.Spy} spy Created spy instance
*/
spyOn : function (object, propertyName) {
var R = Siesta.Resource('Siesta.Test.BDD')
if (!object) { this.warn(R.get('noObject')); return; }
return new Siesta.Test.BDD.Spy({
name : propertyName,
t : this,
hostObject : object,
propertyName : propertyName
})
},
/**
* This method create a standalone spy function, which tracks all calls to it. Tracking is done using the associated
* spy instance, which is available as `and` property. One can use the {@link Siesta.Test.BDD.Spy} class API to
* verify the calls to the spy function.
*
* Example:
var spyFunc = t.createSpy('onadd listener')
myObservable.addEventListener('add', spyFunc)
// do something that triggers the `add` event on the `myObservable`
t.expect(spyFunc).toHaveBeenCalled()
t.expect(spyFunc.calls.argsFor(1)).toEqual([ 'Arg1', 'Arg2' ])
*
* See also: {@link #spyOn}
*
* @param {String} [spyName='James Bond'] A name of the spy for debugging purposes
*
* @return {Function} Created function. The associated spy instance is assigned to it as the `and` property
*/
createSpy : function (spyName) {
return (new Siesta.Test.BDD.Spy({
name : spyName || 'James Bond',
t : this
})).stub().getProcessor()
},
/**
* This method creates an object, which properties are spy functions. Such object can later be used as a mockup.
*
* This method can be called with one argument only, which should be an array of properties.
*
* Example:
var mockup = t.createSpyObj('encoder-mockup', [ 'encode', 'decode' ])
// or just
var mockup = t.createSpyObj([ 'encode', 'decode' ])
mockup.encode('string')
mockup.decode('string')
t.expect(mockup.encode).toHaveBeenCalled()
*
* See also: {@link #createSpy}
*
* @param {String} spyName A name of the spy object. Can be omitted.
* @param {Array[String]} properties An array of the property names. For each property name a spy function will be created.
*
* @return {Object} A mockup object
*/
createSpyObj : function (spyName, properties) {
if (arguments.length == 1) { properties = spyName; spyName = null }
spyName = spyName || 'spyObject'
var me = this
var obj = {}
Joose.A.each(properties, function (propertyName) {
obj[ propertyName ] = me.createSpy(spyName)
})
return obj
}
},
override : {
cleanup : function () {
this.beforeEachHooks = this.afterEachHooks = null
this.SUPER()
},
onTestFinalize : function () {
Joose.A.each(this.spies, function (spy) { spy.remove() })
this.spies = null
this.SUPER()
},
afterLaunch : function () {
this.codeProcessed = true
this.launchSpecs()
this.SUPERARG(arguments)
}
}
})
//eof Siesta.Test.BDD