UNPKG

siesta-lite

Version:

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

411 lines (335 loc) 16.5 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test.Observable This is a mixin, with assertions/ helper methods for testing observable pattern (in NodeJS world known as `EventEmitter`). */ Role('Siesta.Test.Observable', { does : [ Siesta.Util.Role.CanGetType ], requires : [ 'addListenerToObservable', 'removeListenerFromObservable', 'getSourceLine', 'pass', 'fail', 'processCallbackFromTest' ], has : { }, methods : { /** * This method will wait for the first browser `event`, fired by the provided `observable` and will then call the provided callback. * * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector. * @param {String} event The name of the event to wait for * @param {Function} callback The callback to call * @param {Object} scope The scope for the callback * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForEvent : function (observable, event, callback, scope, timeout) { var me = this var R = Siesta.Resource('Siesta.Test.Browser'); var eventFired = false var listener = function () { eventFired = true } this.addListenerToObservable(observable, event, listener) return this.waitFor({ method : function() { return eventFired; }, callback : function () { me.removeListenerFromObservable(observable, event, listener); // `scope` will be already applied callback && callback.apply(this, arguments) }, scope : scope, timeout : timeout, assertionName : 'waitForEvent', description : ' ' + R.get('waitForEvent') + ' "' + event + '" ' + R.get('event') }); }, /** * This assertion verifies the number of certain events fired by the provided observable instance during provided function (possibly `async`) or time period. * * For example: * t.firesOk({ observable : store, events : { update : 1, add : 2, datachanged : '> 1' }, during : function () { store.getAt(0).set('Foo', 'Bar'); store.add({ FooBar : 'BazQuix' }) store.add({ Foo : 'Baz' }) }, desc : 'Correct events fired' }) // or async await t.firesOk({ observable : someObservable, events : { datachanged : '> 1' }, during : async () => { await someObservable.loadData() }, desc : 'Correct events fired' }) // or t.firesOk({ observable : store, events : { update : 1, add : 2, datachanged : '>= 1' }, during : 1 }) store.getAt(0).set('Foo', 'Bar'); store.add({ FooBar : 'BazQuix' }) store.add({ Foo : 'Baz' }) * * Normally this method accepts a single object with various options (as shown above), but also can be called in 2 additional shortcuts forms: * // 1st form for multiple events t.firesOk(observable, { event1 : 1, event2 : '>1' }, description) // 2nd form for single event t.firesOk(observable, eventName, 1, description) t.firesOk(observable, eventName, '>1', description) * * In both forms, `during` is assumed to be undefined and `description` is optional. * * @param {Object} options An obect with the following properties: * @param {Ext.util.Observable/Ext.Element/HTMLElement} options.observable Any browser observable, window object, element instances, CSS selector. * @param {Object} options.events The object, properties of which corresponds to event names and values - to expected * number of this event triggering. If value of some property is a number then exact that number of events is expected. If value * of some property is a string starting with one of the comparison operators like "\<", "\<=", "==" etc and followed by the number * then Siesta will perform that comparison with the number of actualy fired events. * @param {Number/Function} [options.during] If provided as a number denotes the number of milliseconds during which * this assertion will "record" the events from observable, if provided as regular function - then this assertion will "record" * only events fired during execution of this function (`async` functions are supported, in this case, don't forget to `await` * on the assertion call itself). If not provided at all - assertions are recorded until the end of * current test (or sub-test) * @param {Function} [options.callback] A callback to call after this assertion has been checked. Only used if `during` value is provided. * @param {String} [options.desc] A description for this assertion */ firesOk: function (options, events, n, timeOut, func, desc, callback) { // | backward compat arguments | var me = this; var sourceLine = me.getSourceLine(); var R = Siesta.Resource('Siesta.Test.Browser'); var nbrArgs = arguments.length var observable, during if (nbrArgs == 1) { observable = options.observable events = options.events during = options.during desc = options.desc || options.description callback = options.callback timeOut = this.typeOf(during) == 'Number' ? during : null func = /Function/.test(this.typeOf(during)) ? during : null } else if (nbrArgs >= 5) { // old signature, backward compat observable = options if (this.typeOf(events) == 'String') { var obj = {} obj[ events ] = n events = obj } } else if (nbrArgs <= 3 && this.typeOf(events) == 'Object') { // shortcut form 1 observable = options desc = n } else if (nbrArgs <= 4 && this.typeOf(events) == 'String') { // shortcut form 2 observable = options var obj = {} obj[ events ] = n events = obj desc = timeOut timeOut = null } else throw new Error(R.get('unrecognizedSignature')) // start recording var counters = {}; var countFuncs = {}; Joose.O.each(events, function (expected, eventName) { counters[ eventName ] = 0 var countFunc = countFuncs[ eventName ] = function () { counters[ eventName ]++ } me.addListenerToObservable(observable, eventName, countFunc); }) // stop recording and verify the results var stopRecording = function () { Joose.O.each(events, function (expected, eventName) { me.removeListenerFromObservable(observable, eventName, countFuncs[ eventName ]); var actualNumber = counters[ eventName ] if (me.verifyExpectedNumber(actualNumber, expected)) me.pass(desc, { descTpl : R.get('observableFired') + ' ' + actualNumber + ' `' + eventName + '` ' + R.get('events') }); else me.fail(desc, { assertionName : 'firesOk', sourceLine : sourceLine, descTpl : R.get('observableFiredOk') + ' `' + eventName + '` ' + R.get('events'), got : actualNumber, gotDesc : R.get('actualNbrEvents'), need : expected, needDesc : R.get('expectedNbrEvents') }); }) } if (timeOut) { var async = this.beginAsync(timeOut + 100); var originalSetTimeout = this.originalSetTimeout; originalSetTimeout(function () { me.endAsync(async); stopRecording() me.processCallbackFromTest(callback); }, timeOut); } else if (func) { var typeOf = this.typeOf(func) var cont = function () { stopRecording() me.processCallbackFromTest(callback) } if (typeOf === 'Function') { var res = func() if (me.typeOf(res) === 'Promise' || me.global.Promise && (res instanceof me.global.Promise)) { return res.then(cont, cont) } else cont() } else if (typeOf === 'AsyncFunction') { return func().then(cont, cont) } } else { this.on('beforetestfinalizeearly', stopRecording) } }, /** * This assertion passes if the observable fires the specified event exactly (n) times during the test execution. * * @param {Ext.util.Observable/Ext.Element/HTMLElement} observable The observable instance * @param {String} event The name of event * @param {Number} n The expected number of events to be fired * @param {String} [desc] The description of the assertion. */ willFireNTimes: function (observable, event, n, desc, isGreaterEqual) { this.firesOk(observable, event, isGreaterEqual ? '>=' + n : n, desc) }, getObjectWithExpectedEvents : function (event, expected) { var events = {} if (this.typeOf(event) == 'Array') Joose.A.each(event, function (eventName) { events[ eventName ] = expected }) else events[ event ] = expected return events }, /** * This assertion passes if the observable does not fire the specified event(s) after calling this method. * * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector. * @param {String/Array[String]} event The name of event or array of such * @param {String} [desc] The description of the assertion. */ wontFire : function(observable, event, desc) { this.firesOk({ observable : observable, events : this.getObjectWithExpectedEvents(event, 0), desc : desc }); }, /** * This assertion passes if the observable fires the specified event exactly once after calling this method. * * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector. * @param {String/Array[String]} event The name of event or array of such * @param {String} [desc] The description of the assertion. */ firesOnce : function(observable, event, desc) { this.firesOk({ observable : observable, events : this.getObjectWithExpectedEvents(event, 1), desc : desc }); }, /** * Alias for {@link #wontFire} method * * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector. * @param {String/Array[String]} event The name of event or array of such * @param {String} [desc] The description of the assertion. */ isntFired : function() { this.wontFire.apply(this, arguments); }, /** * This assertion passes if the observable fires the specified event at least `n` times after calling this method. * * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector. * @param {String} event The name of event * @param {Number} n The minimum number of events to be fired * @param {String} [desc] The description of the assertion. */ firesAtLeastNTimes : function(observable, event, n, desc) { this.firesOk(observable, event, '>=' + n, desc); }, /** * This assertion will verify that the observable fires the specified event and supplies the correct parameters to the listener function. * A checker method should be supplied that verifies the arguments passed to the listener function, and then returns true or false depending on the result. * If the event was never fired, this assertion fails. If the event is fired multiple times, all events will be checked, but * only one pass/fail message will be reported. * * For example: * t.isFiredWithSignature(store, 'add', function (store, records, index) { return (store instanceof Ext.data.Store) && (records instanceof Array) && t.typeOf(index) == 'Number' }) * @param {Ext.util.Observable/Siesta.Test.ActionTarget} observable Ext.util.Observable instance or target as specified by the {@link Siesta.Test.ActionTarget} rules with * the only difference that component queries will be resolved till the component level, and not the DOM element. * @param {String} event The name of event * @param {Function} checkerFn A method that should verify each argument, and return true or false depending on the result. * @param {String} [desc] The description of the assertion. */ isFiredWithSignature : function(observable, event, checkerFn, description) { var eventFired; var me = this; var sourceLine = me.getSourceLine(); var R = Siesta.Resource('Siesta.Test.ExtJS.Observable'); var verifyFiredFn = function () { me.removeListenerFromObservable(observable, event, listener) if (!eventFired) { me.fail('The [' + event + "] " + R.get('isFiredWithSignatureNotFired')); } }; me.on('beforetestfinalizeearly', verifyFiredFn); var listener = function () { var result = checkerFn.apply(me, arguments); if (!eventFired && result) { me.pass(description || R.get('observableFired') + ' ' + event + ' ' + R.get('correctSignature'), { sourceLine : sourceLine }); } if (!result) { me.fail(description || R.get('observableFired') + ' ' + event + ' ' + R.get('incorrectSignature'), { sourceLine : sourceLine }); // Don't spam the assertion grid with failure, one failure is enough me.removeListenerFromObservable(observable, event, listener) } eventFired = true }; me.addListenerToObservable(observable, event, listener) } } });