From 40d7e66f408eaaa66efd8d7934ab2eb3324236a1 Mon Sep 17 00:00:00 2001 From: Elliott Sprehn Date: Sun, 24 Oct 2010 14:14:45 -0700 Subject: Lots of bug fixes in the scenario runner and a bunch of new features. - By default the runner now creates multiple output formats as it runs. Nodes are created in the DOM with ids: json, xml, and html. ex. $('#json').html() => json output of the runner ex. $('#xml').html() => json output of the runner $result is also an object tree result. The permitted formats are html,json,xml,object. If you don't want certain formats you can select specific ones with the new ng:scenario-output attribute on the script tag. '); + } + + function addCSS(path) { + document.write(''); + } + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularScenarioInit($scenario, angularJsConfig(document)); + }; + + addCSS("../../css/angular-scenario.css"); + addScript("../../lib/jquery/jquery-1.4.2.js"); + document.write( + '' + ); + addScript("../angular-bootstrap.js"); + + addScript("Scenario.js"); + addScript("Application.js"); + addScript("Describe.js"); + addScript("Future.js"); + addScript("Runner.js"); + addScript("SpecRunner.js"); + addScript("dsl.js"); + addScript("matchers.js"); + addScript("ObjectModel.js"); + addScript("output/Html.js"); + addScript("output/Json.js"); + addScript("output/Object.js"); + addScript("output/Xml.js"); + + // Create the runner (which also sets up the global API) + document.write( + '' + ); + +})(window.onload); diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix index d6660d61..fb9ae147 100644 --- a/src/scenario/angular.prefix +++ b/src/scenario/angular.prefix @@ -22,4 +22,4 @@ * THE SOFTWARE. */ (function(window, document, previousOnLoad){ - var _jQuery = window.jQuery.noConflict(true); \ No newline at end of file + var _jQuery = window.jQuery.noConflict(true); diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix index c38f0ab5..66843013 100644 --- a/src/scenario/angular.suffix +++ b/src/scenario/angular.suffix @@ -4,25 +4,7 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - _jQuery(document.body).append( - '
' + - '
' - ); - var frame = _jQuery('#frame'); - var runner = _jQuery('#runner'); - var application = new angular.scenario.Application(frame); - var ui = new angular.scenario.ui.Html(runner); - $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { - frame.remove(); - if (error) { - if (window.console) { - console.log(error.stack || error); - } else { - // Do something for IE - alert(error); - } - } - }); + angularScenarioInit($scenario, angularJsConfig(document)); }; })(window, document, window.onload); diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js deleted file mode 100644 index 4661bfb2..00000000 --- a/src/scenario/bootstrap.js +++ /dev/null @@ -1,73 +0,0 @@ -(function(previousOnLoad){ - var prefix = (function(){ - var filename = /(.*\/)bootstrap.js(#(.*))?/; - var scripts = document.getElementsByTagName("script"); - for(var j = 0; j < scripts.length; j++) { - var src = scripts[j].src; - if (src && src.match(filename)) { - var parts = src.match(filename); - return parts[1]; - } - } - })(); - - function addScript(path) { - document.write(''); - } - - function addCSS(path) { - document.write(''); - } - - window.onload = function(){ - try { - if (previousOnLoad) previousOnLoad(); - } catch(e) {} - _jQuery(document.body).append( - '
' + - '
' - ); - var frame = _jQuery('#frame'); - var runner = _jQuery('#runner'); - var application = new angular.scenario.Application(frame); - var ui = new angular.scenario.ui.Html(runner); - $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { - frame.remove(); - if (error) { - if (window.console) { - console.log(error.stack || error); - } else { - // Do something for IE - alert(error); - } - } - }); - }; - - addCSS("../../css/angular-scenario.css"); - addScript("../../lib/jquery/jquery-1.4.2.js"); - document.write( - '' - ); - addScript("../angular-bootstrap.js"); - - addScript("Scenario.js"); - addScript("Application.js"); - addScript("Describe.js"); - addScript("Future.js"); - addScript("HtmlUI.js"); - addScript("Runner.js"); - addScript("SpecRunner.js"); - addScript("dsl.js"); - addScript("matchers.js"); - - // Create the runner (which also sets up the global API) - document.write( - '' - ); - -})(window.onload); diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index f4484df8..1ae26db8 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -1,18 +1,19 @@ /** * Shared DSL statements that are useful to all scenarios. */ - + /** * Usage: * wait() waits until you call resume() in the console */ - angular.scenario.dsl('wait', function() { +angular.scenario.dsl('wait', function() { return function() { - return this.addFuture('waiting for you to call resume() in the console', function(done) { + return this.addFuture('waiting for you to resume', function(done) { + this.emit('InteractiveWait', this.spec, this.step); this.$window.resume = function() { done(); }; }); }; - }); +}); /** * Usage: @@ -21,7 +22,7 @@ angular.scenario.dsl('pause', function() { return function(time) { return this.addFuture('pause for ' + time + ' seconds', function(done) { - this.setTimeout(function() { done(null, time * 1000); }, time * 1000); + this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000); }); }; }); @@ -49,8 +50,8 @@ angular.scenario.dsl('expect', function() { /** * Usage: - * navigateTo(future|string) where url a string or future with a value - * of a URL to navigate to + * navigateTo(url) Loads the url into the frame + * navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to */ angular.scenario.dsl('navigateTo', function() { return function(url, delegate) { @@ -60,17 +61,7 @@ angular.scenario.dsl('navigateTo', function() { url = delegate.call(this, url); } application.navigateTo(url, function() { - application.executeAction(function($window) { - if ($window.angular) { - var $browser = $window.angular.service.$browser(); - $browser.poll(); - $browser.notifyWhenNoOutstandingRequests(function() { - done(null, url); - }); - } else { - done(null, url); - } - }); + done(null, url); }); }); }; @@ -162,7 +153,11 @@ angular.scenario.dsl('repeater', function() { chain.count = function() { return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) { - done(null, $document.elements().size()); + try { + done(null, $document.elements().length); + } catch (e) { + done(null, 0); + } }); }; @@ -238,6 +233,7 @@ angular.scenario.dsl('select', function() { /** * Usage: + * element(selector).count() get the number of elements that match selector * element(selector).click() clicks an element * element(selector).attr(name) gets the value of an attribute * element(selector).attr(name, value) sets the value of an attribute @@ -248,10 +244,28 @@ angular.scenario.dsl('select', function() { angular.scenario.dsl('element', function() { var chain = {}; + chain.count = function() { + return this.addFutureAction('element ' + this.selector + ' count', function($window, $document, done) { + try { + done(null, $document.elements().length); + } catch (e) { + done(null, 0); + } + }); + }; + chain.click = function() { return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) { - $document.elements().trigger('click'); - done(); + var elements = $document.elements(); + var href = elements.attr('href'); + elements.trigger('click'); + if (href && elements[0].nodeName.toUpperCase() === 'A') { + this.application.navigateTo(href, function() { + done(); + }); + } else { + done(); + } }); }; diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js index 0dfbc455..8ef154e9 100644 --- a/src/scenario/matchers.js +++ b/src/scenario/matchers.js @@ -6,6 +6,10 @@ angular.scenario.matcher('toEqual', function(expected) { return angular.equals(this.actual, expected); }); +angular.scenario.matcher('toBe', function(expected) { + return this.actual === expected; +}); + angular.scenario.matcher('toBeDefined', function() { return angular.isDefined(this.actual); }); diff --git a/src/scenario/output/Html.js b/src/scenario/output/Html.js new file mode 100644 index 00000000..4a682b9a --- /dev/null +++ b/src/scenario/output/Html.js @@ -0,0 +1,165 @@ +/** + * User Interface for the Scenario Runner. + * + * TODO(esprehn): This should be refactored now that ObjectModel exists + * to use angular bindings for the UI. + */ +angular.scenario.output('html', function(context, runner) { + var model = new angular.scenario.ObjectModel(runner); + + context.append( + '' + + '
' + + '
' + + '
' + ); + + runner.on('InteractiveWait', function(spec, step) { + var ui = model.getSpec(spec.id).getLastStep().ui; + ui.find('.test-title'). + html('waiting for you to resume.'); + }); + + runner.on('SpecBegin', function(spec) { + var ui = findContext(spec); + ui.find('> .tests').append( + '
  • ' + ); + ui = ui.find('> .tests li:last'); + ui.append( + '
    ' + + '

    ' + + ' ' + + ' ' + + '

    ' + + '
    ' + + '
    ' + + '
      ' + + '
      ' + ); + ui.find('> .test-info .test-name').text(spec.name); + ui.find('> .test-info').click(function() { + var scrollpane = ui.find('> .scrollpane'); + var actions = scrollpane.find('> .test-actions'); + var name = context.find('> .test-info .test-name'); + if (actions.find(':visible').length) { + actions.hide(); + name.removeClass('open').addClass('closed'); + } else { + actions.show(); + scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); + name.removeClass('closed').addClass('open'); + } + }); + model.getSpec(spec.id).ui = ui; + }); + + runner.on('SpecError', function(spec, error) { + var ui = model.getSpec(spec.id).ui; + ui.append('
      ');
      +    ui.find('> pre').text(formatException(error));
      +  });
      +
      +  runner.on('SpecEnd', function(spec) {
      +    spec = model.getSpec(spec.id);
      +    spec.ui.removeClass('status-pending');
      +    spec.ui.addClass('status-' + spec.status);
      +    spec.ui.find("> .test-info .timer-result").text(spec.duration + "ms");
      +    if (spec.status === 'success') {
      +      spec.ui.find('> .test-info .test-name').addClass('closed');
      +      spec.ui.find('> .scrollpane .test-actions').hide();
      +    }
      +    updateTotals(spec.status);
      +  });
      +
      +  runner.on('StepBegin', function(spec, step) {
      +    spec = model.getSpec(spec.id);
      +    step = spec.getLastStep();
      +    spec.ui.find('> .scrollpane .test-actions').
      +      append('
    1. '); + step.ui = spec.ui.find('> .scrollpane .test-actions li:last'); + step.ui.append( + '
      ' + + '
      ' + ); + step.ui.find('> .test-title').text(step.name); + var scrollpane = step.ui.parents('.scrollpane'); + scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); + }); + + runner.on('StepFailure', function(spec, step, error) { + var ui = model.getSpec(spec.id).getLastStep().ui; + addError(ui, step.line, error); + }); + + runner.on('StepError', function(spec, step, error) { + var ui = model.getSpec(spec.id).getLastStep().ui; + addError(ui, step.line, error); + }); + + runner.on('StepEnd', function(spec, step) { + spec = model.getSpec(spec.id); + step = spec.getLastStep(); + step.ui.find('.timer-result').text(step.duration + 'ms'); + step.ui.removeClass('status-pending'); + step.ui.addClass('status-' + step.status); + var scrollpane = spec.ui.find('> .scrollpane'); + scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); + }); + + /** + * Finds the context of a spec block defined by the passed definition. + * + * @param {Object} The definition created by the Describe object. + */ + function findContext(spec) { + var currentContext = context.find('#specs'); + angular.foreach(model.getDefinitionPath(spec), function(defn) { + var id = 'describe-' + defn.id; + if (!context.find('#' + id).length) { + currentContext.find('> .test-children').append( + '
      ' + + '

      ' + + '
      ' + + ' ' + + '
      ' + ); + context.find('#' + id).find('> h2').text('describe: ' + defn.name); + } + currentContext = context.find('#' + id); + }); + return context.find('#describe-' + spec.definition.id); + }; + + /** + * Updates the test counter for the status. + * + * @param {string} the status. + */ + function updateTotals(status) { + var legend = context.find('#status-legend .status-' + status); + var parts = legend.text().split(' '); + var value = (parts[0] * 1) + 1; + legend.text(value + ' ' + parts[1]); + } + + /** + * Add an error to a step. + * + * @param {Object} The JQuery wrapped context + * @param {Function} fn() that should return the file/line number of the error + * @param {Object} the error. + */ + function addError(context, line, error) { + context.find('.test-title').append('
      ');
      +    var message = _jQuery.trim(line() + '\n\n' + formatException(error));
      +    context.find('.test-title pre:last').text(message);
      +  };
      +});
      diff --git a/src/scenario/output/Json.js b/src/scenario/output/Json.js
      new file mode 100644
      index 00000000..94212301
      --- /dev/null
      +++ b/src/scenario/output/Json.js
      @@ -0,0 +1,10 @@
      +/**
      + * Generates JSON output into a context.
      + */
      +angular.scenario.output('json', function(context, runner) {
      +  var model = new angular.scenario.ObjectModel(runner);
      +  
      +  runner.on('RunnerEnd', function() {
      +    context.text(angular.toJson(model.value));
      +  });
      +});
      diff --git a/src/scenario/output/Object.js b/src/scenario/output/Object.js
      new file mode 100644
      index 00000000..3257cfd7
      --- /dev/null
      +++ b/src/scenario/output/Object.js
      @@ -0,0 +1,6 @@
      +/**
      + * Creates a global value $result with the result of the runner.
      + */
      +angular.scenario.output('object', function(context, runner) {
      +  runner.$window.$result = new angular.scenario.ObjectModel(runner).value;
      +});
      diff --git a/src/scenario/output/Xml.js b/src/scenario/output/Xml.js
      new file mode 100644
      index 00000000..47d98c78
      --- /dev/null
      +++ b/src/scenario/output/Xml.js
      @@ -0,0 +1,48 @@
      +/**
      + * Generates XML output into a context.
      + */
      +angular.scenario.output('xml', function(context, runner) {
      +  var model = new angular.scenario.ObjectModel(runner);
      +
      +  runner.on('RunnerEnd', function() {
      +    context.append('');
      +    serializeXml(context.find('> scenario'), model.value);
      +  });
      +
      +  /**
      +   * Convert the tree into XML.
      +   *
      +   * @param {Object} context jQuery context to add the XML to.
      +   * @param {Object} tree node to serialize
      +   */
      +  function serializeXml(context, tree) {
      +     angular.foreach(tree.children, function(child) {
      +       context.append('');
      +       var describeContext = context.find('> describe:last');
      +       describeContext.attr('id', child.id);
      +       describeContext.attr('name', child.name);
      +       serializeXml(describeContext, child);
      +     });
      +     context.append('');
      +     context = context.find('> its');
      +     angular.foreach(tree.specs, function(spec) {
      +       context.append('')
      +       var specContext = context.find('> it:last');
      +       specContext.attr('id', spec.id);
      +       specContext.attr('name', spec.name);
      +       specContext.attr('duration', spec.duration);
      +       specContext.attr('status', spec.status);
      +       angular.foreach(spec.steps, function(step) {
      +         specContext.append('');
      +         var stepContext = specContext.find('> step:last');
      +         stepContext.attr('name', step.name);
      +         stepContext.attr('duration', step.duration);
      +         stepContext.attr('status', step.status);
      +         if (step.error) {
      +           stepContext.append('