'use strict'; /** * Setup file for the Scenario. * Must be first in the compilation/bootstrap list. */ // Public namespace angular.scenario = angular.scenario || {}; /** * Defines a new output format. * * @param {string} name the name of the new output format * @param {function()} fn function(context, runner) that generates the output */ angular.scenario.output = angular.scenario.output || function(name, fn) { angular.scenario.output[name] = fn; }; /** * Defines a new DSL statement. If your factory function returns a Future * it's returned, otherwise the result is assumed to be a map of functions * for chaining. Chained functions are subject to the same rules. * * Note: All functions on the chain are bound to the chain scope so values * set on "this" in your statement function are available in the chained * functions. * * @param {string} name The name of the statement * @param {function()} fn Factory function(), return a function for * the statement. */ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) { angular.scenario.dsl[name] = function() { function executeStatement(statement, args) { var result = statement.apply(this, args); if (angular.isFunction(result) || result instanceof angular.scenario.Future) return result; var self = this; var chain = angular.extend({}, result); angular.forEach(chain, function(value, name) { if (angular.isFunction(value)) { chain[name] = function() { return executeStatement.call(self, value, arguments); }; } else { chain[name] = value; } }); return chain; } var statement = fn.apply(this, arguments); return function() { return executeStatement.call(this, statement, arguments); }; }; }; /** * Defines a new matcher for use with the expects() statement. The value * this.actual (like in Jasmine) is available in your matcher to compare * against. Your function should return a boolean. The future is automatically * created for you. * * @param {string} name The name of the matcher * @param {function()} fn The matching function(expected). */ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) { angular.scenario.matcher[name] = function(expected) { var prefix = 'expect ' + this.future.name + ' '; if (this.inverse) { prefix += 'not '; } var self = this; this.addFuture(prefix + name + ' ' + angular.toJson(expected), function(done) { var error; self.actual = self.future.value; if ((self.inverse && fn.call(self, expected)) || (!self.inverse && !fn.call(self, expected))) { error = 'expected ' + angular.toJson(expected) + ' but was ' + angular.toJson(self.actual); } done(error); }); }; }; /** * Initialize the scenario runner and run ! * * Access global window and document object * Access $runner through closure * * @param {Object=} config Config options */ angular.scenario.setUpAndRun = function(config) { var href = window.location.href; var body = _jQuery(document.body); var output = []; var objModel = new angular.scenario.ObjectModel($runner); if (config && config.scenario_output) { output = config.scenario_output.split(','); } angular.forEach(angular.scenario.output, function(fn, name) { if (!output.length || indexOf(output,name) != -1) { var context = body.append('<div></div>').find('div:last'); context.attr('id', name); fn.call({}, context, $runner, objModel); } }); if (!/^http/.test(href) && !/^https/.test(href)) { body.append('<p id="system-error"></p>'); body.find('#system-error').text( 'Scenario runner must be run using http or https. The protocol ' + href.split(':')[0] + ':// is not supported.' ); return; } var appFrame = body.append('<div id="application"></div>').find('#application'); var application = new angular.scenario.Application(appFrame); $runner.on('RunnerEnd', function() { appFrame.css('display', 'none'); appFrame.find('iframe').attr('src', 'about:blank'); }); $runner.on('RunnerError', function(error) { if (window.console) { console.log(formatException(error)); } else { // Do something for IE alert(error); } }); $runner.run(application); }; /** * Iterates through list with iterator function that must call the * continueFunction to continue iterating. * * @param {Array} list list to iterate over * @param {function()} iterator Callback function(value, continueFunction) * @param {function()} done Callback function(error, result) called when * iteration finishes or an error occurs. */ function asyncForEach(list, iterator, done) { var i = 0; function loop(error, index) { if (index && index > i) { i = index; } if (error || i >= list.length) { done(error); } else { try { iterator(list[i++], loop); } catch (e) { done(e); } } } loop(); } /** * Formats an exception into a string with the stack trace, but limits * to a specific line length. * * @param {Object} error The exception to format, can be anything throwable * @param {Number=} [maxStackLines=5] max lines of the stack trace to include * default is 5. */ function formatException(error, maxStackLines) { maxStackLines = maxStackLines || 5; var message = error.toString(); if (error.stack) { var stack = error.stack.split('\n'); if (stack[0].indexOf(message) === -1) { maxStackLines++; stack.unshift(error.message); } message = stack.slice(0, maxStackLines).join('\n'); } return message; } /** * Returns a function that gets the file name and line number from a * location in the stack if available based on the call site. * * Note: this returns another function because accessing .stack is very * expensive in Chrome. * * @param {Number} offset Number of stack lines to skip */ function callerFile(offset) { var error = new Error(); return function() { var line = (error.stack || '').split('\n')[offset]; // Clean up the stack trace line if (line) { if (line.indexOf('@') !== -1) { // Firefox line = line.substring(line.indexOf('@')+1); } else { // Chrome line = line.substring(line.indexOf('(')+1).replace(')', ''); } } return line || ''; }; } /** * Don't use the jQuery trigger method since it works incorrectly. * * jQuery notifies listeners and then changes the state of a checkbox and * does not create a real browser event. A real click changes the state of * the checkbox and then notifies listeners. * * To work around this we instead use our own handler that fires a real event. */ (function(fn){ var parentTrigger = fn.trigger; fn.trigger = function(type) { if (/(click|change|keydown|blur|input|mousedown|mouseup)/.test(type)) { var processDefaults = []; this.each(function(index, node) { processDefaults.push(browserTrigger(node, type)); }); // this is not compatible with jQuery - we return an array of returned values, // so that scenario runner know whether JS code has preventDefault() of the event or not... return processDefaults; } return parentTrigger.apply(this, arguments); }; })(_jQuery.fn); /** * Finds all bindings with the substring match of name and returns an * array of their values. * * @param {string} bindExp The name to match * @return {Array.<string>} String of binding values */ _jQuery.fn.bindings = function(windowJquery, bindExp) { var result = [], match, bindSelector = '.ng-binding:visible'; if (angular.isString(bindExp)) { bindExp = bindExp.replace(/\s/g, ''); match = function (actualExp) { if (actualExp) { actualExp = actualExp.replace(/\s/g, ''); if (actualExp == bindExp) return true; if (actualExp.indexOf(bindExp) == 0) { return actualExp.charAt(bindExp.length) == '|'; } } } } else if (bindExp) { match = function(actualExp) { return actualExp && bindExp.exec(actualExp); } } else { match = function(actualExp) { return !!actualExp; }; } var selection = this.find(bindSelector); if (this.is(bindSelector)) { selection = selection.add(this); } function push(value) { if (value == undefined) { value = ''; } else if (typeof value != 'string') { value = angular.toJson(value); } result.push('' + value); } selection.each(function() { var element = windowJquery(this), binding; if (binding = element.data('$binding')) { if (typeof binding == 'string') { if (match(binding)) { push(element.scope().$eval(binding)); } } else { if (!angular.isArray(binding)) { binding = [binding]; } for(var fns, j=0, jj=binding.length; j<jj; j++) { fns = binding[j]; if (fns.parts) { fns = fns.parts; } else { fns = [fns]; } for (var scope, fn, i = 0, ii = fns.length; i < ii; i++) { if(match((fn = fns[i]).exp)) { push(fn(scope = scope || element.scope())); } } } } } }); return result; };