siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
478 lines (375 loc) • 19.6 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.Expectation
This class is the central point for writing assertions in BDD style. Instances of this class can be generated with the {@link Siesta.Test#expect expect}
method. Then, calling some method on the instance will create a new assertion in the test.
* **Note**, that to negate any assertion, you can use a special property {@link #not}, that contains an expectation instance with the opposite 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')
*/
Class('Siesta.Test.BDD.Expectation', {
does : [
Siesta.Util.Role.CanGetType
],
has : {
value : null,
isNot : false,
/**
* @property {Siesta.Test.BDD.Expectation} not Another expectation instance with the negated meaning.
*/
not : null,
t : null
},
methods : {
initialize : function () {
if (!this.isNot) this.not = new this.constructor({
isNot : true,
t : this.t,
value : this.value
})
},
process : function (passed, config) {
var isNot = this.isNot
config = config || {}
config.not = config.not || isNot ? 'not ' : ''
config.got = config.hasOwnProperty('got') ? config.got : this.value
if (config.noGot) delete config.got
var assertionName = config.assertionName
if (assertionName && isNot) config.assertionName = assertionName.replace(/^(expect\(.+?\)\.)/, '$1not.')
passed = isNot ? !passed : passed
this.t[ passed ? 'pass' : 'fail' ](null, config)
},
/**
* This assertion compares the value provided to the {@link Siesta.Test#expect expect} method with the `expectedValue` argument.
* Comparison is done with `===` operator, so it should be used **only with the primitivies** - numbers, strings, booleans etc.
*
* To deeply compare Date, Arrays and JSON objects in general, use {@link #toEqual} method.
*
* This method works correctly with the placeholders generated with {@link Siesta.Test#any any} method
*
* @param {Primitive} expectedValue An expected value
*/
toBe : function (expectedValue) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.t.compareObjects(this.value, expectedValue, true, true), {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' {need}',
assertionName : 'expect(got).toBe(need)',
need : expectedValue,
needDesc : this.isNot ? R.get('needNotText') : R.get('needText')
})
},
/**
* This assertion compares the value provided to the {@link Siesta.Test#expect expect} method with the `expectedValue` argument.
*
* Comparison works for Date, Array, and JSON objects in general. It is performed "deeply".
* Right now the values should not contain cyclic references.
*
* This method works correctly with the placeholders generated with {@link Siesta.Test#any any} method
*
* @param {Mixed} expectedValue An expected value
*/
toEqual : function (expectedValue) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.t.compareObjects(this.value, expectedValue, true), {
descTpl : R.get('expectText') +' {got} {!not}' + R.get('toBeEqualToText') + ' {need}',
assertionName : 'expect(got).toEqual(need)',
need : expectedValue,
needDesc : this.isNot ? R.get('needNotText') : R.get('needText')
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is `null`.
*/
toBeNull : function () {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.t.compareObjects(this.value, null, true, true), {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' null',
assertionName : 'expect(got).toBeNull()',
need : null,
needDesc : this.isNot ? R.get('needNotText') : R.get('needText')
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is `NaN`.
*/
toBeNaN : function () {
var value = this.value
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.t.typeOf(value) == 'Number' && value != value, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' NaN',
assertionName : 'expect(got).toBeNaN()',
need : NaN,
needDesc : this.isNot ? R.get('needNotText') : R.get('needText')
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is not the `undefined` value.
*/
toBeDefined : function () {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.value !== undefined, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeDefinedText'),
assertionName : 'expect(got).toBeDefined()'
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is the `undefined` value.
*/
toBeUndefined : function (value) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.value === undefined, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeUndefinedText'),
assertionName : 'expect(got).toBeUndefined()'
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is "truthy" - evaluates to `true`.
* For example - non empty strings, numbers except the 0, objects, arrays etc.
*/
toBeTruthy : function () {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.value, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeTruthyText'),
assertionName : 'expect(got).toBeTruthy()'
})
},
/**
* This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is "falsy" - evaluates to `false`.
* For example - empty strings, number 0, `null`, `undefined`, etc.
*/
toBeFalsy : function () {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(!this.value, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeFalsyText'),
assertionName : 'expect(got).toBeFalsy()'
})
},
/**
* This assertion passes, when the string provided to the {@link Siesta.Test#expect expect} method matches the regular expression.
*
* @param {RegExp} regexp The regular expression to match the string against
*/
toMatch : function (regexp) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
if (this.t.typeOf(regexp) != 'RegExp') throw new Error("`expect().toMatch()` matcher expects a regular expression")
this.process(new RegExp(regexp).test(this.value), {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toMatchText') + ' {need}',
assertionName : 'expect(got).toMatch(need)',
need : regexp,
needDesc : this.isNot ? R.get('needNotMatchingText') : R.get('needMatchingText')
})
},
/**
* This assertion passes in 2 cases:
*
* 1) When the value provided to the {@link Siesta.Test#expect expect} method is a string, and it contains a passed substring.
* 2) When the value provided to the {@link Siesta.Test#expect expect} method is an array (or array-like), and it contains a passed element.
*
* @param {String/Mixed} element The element of the array or a sub-string
*/
toContain : function (element) {
var value = this.value
var t = this.t
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
var passed = false
if (t.typeOf(value) == 'String') {
this.process(value.indexOf(element) >= 0, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toContainText') + ' {need}',
assertionName : 'expect(got).toContain(need)',
need : element,
needDesc : this.isNot ? R.get('needStringNotContainingText') : R.get('needStringContainingText')
})
} else {
// Normalize to allow NodeList, Arguments etc.
value = Array.prototype.slice.call(value);
for (var i = 0; i < value.length; i++)
if (t.compareObjects(element, value[ i ], true)) {
passed = true
break
}
this.process(passed, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toContainText') + ' {need}',
assertionName : 'expect(got).toContain(need)',
need : element,
needDesc : this.isNot ? R.get('needArrayNotContainingText') : R.get('needArrayContainingText')
})
}
},
/**
* This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is less than the
* expected number.
*
* @param {Number} expectedValue The number to compare with
*/
toBeLessThan : function (expectedValue) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.value < expectedValue, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeLessThanText') + ' {need}',
assertionName : 'expect(got).toBeLessThan(need)',
need : expectedValue,
needDesc : this.isNot ? R.get('needGreaterEqualThanText') : R.get('needLessThanText')
})
},
/**
* This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is greater than the
* expected number.
*
* @param {Number} expectedValue The number to compare with
*/
toBeGreaterThan : function (expectedValue) {
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(this.value > expectedValue, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeGreaterThanText') + ' {need}',
assertionName : 'expect(got).toBeGreaterThan(need)',
need : expectedValue,
needDesc : this.isNot ? R.get('needLessEqualThanText') : R.get('needGreaterThanText')
})
},
/**
* This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is approximately equal
* the given number. The proximity can be defined as the `precision` argument
*
* @param {Number} expectedValue The number to compare with
* @param {Number} [precision=2] The number of digits after dot (comma) that should be same in both numbers.
*/
toBeCloseTo : function (expectedValue, precision) {
precision = precision != null ? precision : 2
// not sure why we divide the precision by 2, but jasmine does that for some reason
var threshold = Math.pow(10, -precision) / 2
var delta = Math.abs(this.value - expectedValue)
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
this.process(delta < threshold, {
descTpl : R.get('expectText') + ' {got} {!not}' + R.get('toBeCloseToText') +' {need}',
assertionName : 'expect(got).toBeCloseTo(need)',
need : expectedValue,
needDesc : this.isNot ? R.get('needValueNotCloseToText') : R.get('needValueCloseToText'),
annotation : delta ? R.get('thresholdIsText') + threshold : R.get('exactMatchText')
})
},
/**
* This assertion passes when the function provided to the {@link Siesta.Test#expect expect} method, throws an exception
* during its execution.
*
* t.expect(function(){
* throw "oopsie";
* }).toThrow());
*
*/
toThrow : function () {
var func = this.value
var t = this.t
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
if (t.typeOf(func) != 'Function') throw new Error("`expect().toMatch()` matcher expects a function")
var e = t.getExceptionCatcher()(func)
if (e instanceof t.getTestErrorClass())
//IE uses non-standard 'description' property for error msg
e = e.message || e.description
this.process(e !== undefined, {
descTpl : R.get('expectText') + ' function {!not}' + R.get('toThrowText'),
assertionName : 'expect(func).toThrow()',
annotation : e ? (R.get('thrownExceptionText') + ': ' + Siesta.Util.Serializer.stringify(e)) : R.get('noExceptionThrownText'),
noGot : true
})
},
/**
* This assertion passes, if a spy, provided to the {@link Siesta.Test#expect expect} method have been
* called expected number of times. The expected number of times can be provided as the 1st argument and by default
* is 1.
*
* One can also provide the function, spied on, to the {@link Siesta.Test#expect expect} method.
*
* Examples:
*
var spy = t.spyOn(obj, 'process')
// call the method 2 times
obj.process()
obj.process()
// following 2 calls are equivalent
t.expect(spy).toHaveBeenCalled();
t.expect(obj.process).toHaveBeenCalled();
// one can also use exact number of calls or comparison operators
t.expect(obj.process).toHaveBeenCalled(2);
t.expect(obj.process).toHaveBeenCalled('>1');
t.expect(obj.process).toHaveBeenCalled('<=3');
*
* See also {@link #toHaveBeenCalledWith}
*
* @param {Number/String} expectedNumber Expected number of calls. Can be either a number, specifying the exact
* number of calls, or a string. In the latter case one can include a comparison operator in front of the number.
*
*/
toHaveBeenCalled : function (expectedNumber) {
expectedNumber = expectedNumber != null ? expectedNumber : '>=1'
var spy = this.value
var t = this.t
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
if (this.typeOf(spy) == 'Function') {
if (!spy.__SIESTA_SPY__) throw new Error(R.get('wrongSpy'))
spy = spy.__SIESTA_SPY__
}
if (!(spy instanceof Siesta.Test.BDD.Spy)) throw new Error(R.get('wrongSpy'))
this.process(t.verifyExpectedNumber(spy.callsLog.length, expectedNumber), {
descTpl : R.get('toHaveBeenCalledDescTpl'),
assertionName : 'expect(func).toHaveBeenCalled()',
methodName : spy.propertyName || '[function]',
got : spy.callsLog.length,
gotDesc : R.get('actualNbrOfCalls'),
need : (this.isNot ? 'not ' : '') + expectedNumber,
needDesc : R.get('expectedNbrOfCalls')
})
},
/**
* This assertion passes, if a spy, provided to the {@link Siesta.Test#expect expect} method have been
* called at least once with the specified arguments.
*
* One can also provide the function, spied on, to the {@link Siesta.Test#expect expect} method.
*
* One can use placeholders, generated with the {@link Siesta.Test.BDD#any any} method to verify the arguments.
*
* Example:
*
var spy = t.spyOn(obj, 'process')
// call the method 2 times with different arguments
obj.build('development', '1.0.0')
obj.build('release', '1.0.1')
t.expect(spy).toHaveBeenCalledWith('development', '1.0.0');
// or
t.expect(obj.process).toHaveBeenCalledWith('development', t.any(String));
*
* See also {@link #toHaveBeenCalled}
*
* @param {Object} arg1 Argument to a call
* @param {Object} arg2 Argument to a call
* @param {Object} argN Argument to a call
*/
toHaveBeenCalledWith : function () {
var spy = this.value
var t = this.t
var R = Siesta.Resource('Siesta.Test.BDD.Expectation');
if (this.typeOf(spy) == 'Function') {
if (!spy.__SIESTA_SPY__) throw new Error(R.get('wrongSpy'))
spy = spy.__SIESTA_SPY__
}
if (!(spy instanceof Siesta.Test.BDD.Spy)) throw new Error(R.get('wrongSpy'))
var args = Array.prototype.slice.call(arguments)
var foundCallWithMatchingArgs = false
Joose.A.each(spy.callsLog, function (call) {
if (t.compareObjects(call.args, args)) { foundCallWithMatchingArgs = true; return false }
})
this.process(foundCallWithMatchingArgs, {
descTpl : R.get('toHaveBeenCalledWithDescTpl'),
assertionName : 'expect(func).toHaveBeenCalledWith()',
methodName : spy.propertyName,
noGot : true
})
}
}
})