From 03df6cbddbb80186caf571e29957370b2ef9881c Mon Sep 17 00:00:00 2001
From: Elliott Sprehn
Date: Fri, 8 Oct 2010 16:43:40 -0700
Subject: New Angular Scenario runner and DSL system with redesigned HTML UI.
Uses the Jasmine syntax for tests, ex:
describe('widgets', function() {
  it('should verify that basic widgets work', function(){
    navigateTo('widgets.html');
    input('text.basic').enter('Carlos');
    expect(binding('text.basic')).toEqual('Carlos');
    input('text.basic').enter('Carlos Santana');
    expect(binding('text.basic')).not().toEqual('Carlos Boozer');
    input('text.password').enter('secret');
    expect(binding('text.password')).toEqual('secret');
    expect(binding('text.hidden')).toEqual('hiddenValue');
    expect(binding('gender')).toEqual('male');
    input('gender').select('female');
    expect(binding('gender')).toEqual('female');
  });
});
Note: To create new UI's implement the interface shown in angular.scenario.ui.Html.
---
 Rakefile                         | 109 +++++++-----
 css/angular-scenario.css         | 211 +++++++++++++++++-----
 jsTestDriver-jquery.conf         |   2 +-
 jsTestDriver.conf                |   2 +-
 scenario/style.css               |   4 +
 scenario/widgets-scenario.js     |  73 +++++---
 src/Angular.js                   |   4 +-
 src/scenario/Application.js      |  51 ++++++
 src/scenario/DSL.js              | 249 +++++++++++++-------------
 src/scenario/Describe.js         | 108 ++++++++++++
 src/scenario/Future.js           |  23 ++-
 src/scenario/HtmlUI.js           | 204 ++++++++++++++++++++++
 src/scenario/Matcher.js          |  21 ---
 src/scenario/Runner.js           | 262 +++++++++------------------
 src/scenario/Scenario.js         | 103 +++++++++++
 src/scenario/SpecRunner.js       |  78 +++++++++
 src/scenario/angular.prefix      |   6 -
 src/scenario/angular.suffix      |  28 ++-
 src/scenario/bootstrap.js        |  62 +++++--
 src/scenario/matchers.js         |  39 +++++
 test/AngularSpec.js              |   4 +
 test/scenario/ApplicationSpec.js |  75 ++++++++
 test/scenario/DSLSpec.js         | 369 ++++++++++++++++++++++-----------------
 test/scenario/DescribeSpec.js    |  85 +++++++++
 test/scenario/FutureSpec.js      |  38 ++++
 test/scenario/HtmlUISpec.js      |  87 +++++++++
 test/scenario/MatcherSpec.js     |  38 ----
 test/scenario/RunnerSpec.js      | 302 +++++++++-----------------------
 test/scenario/SpecRunnerSpec.js  | 165 +++++++++++++++++
 test/scenario/TestContext.js     |  15 --
 test/scenario/matchersSpec.js    |  43 +++++
 test/testabilityPatch.js         |  19 ++
 32 files changed, 1979 insertions(+), 900 deletions(-)
 create mode 100644 src/scenario/Application.js
 create mode 100644 src/scenario/Describe.js
 create mode 100644 src/scenario/HtmlUI.js
 delete mode 100644 src/scenario/Matcher.js
 create mode 100644 src/scenario/Scenario.js
 create mode 100644 src/scenario/SpecRunner.js
 create mode 100644 src/scenario/matchers.js
 create mode 100644 test/scenario/ApplicationSpec.js
 create mode 100644 test/scenario/DescribeSpec.js
 create mode 100644 test/scenario/FutureSpec.js
 create mode 100644 test/scenario/HtmlUISpec.js
 delete mode 100644 test/scenario/MatcherSpec.js
 create mode 100644 test/scenario/SpecRunnerSpec.js
 delete mode 100644 test/scenario/TestContext.js
 create mode 100644 test/scenario/matchersSpec.js
diff --git a/Rakefile b/Rakefile
index 66a1e77b..fc20dc01 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,5 +1,46 @@
 include FileUtils
 
+ANGULAR = [
+  'src/Angular.js',
+  'src/JSON.js',
+  'src/Compiler.js',
+  'src/Scope.js',
+  'src/Injector.js',
+  'src/Parser.js',
+  'src/Resource.js',
+  'src/Browser.js',
+  'src/jqLite.js',
+  'src/apis.js',
+  'src/filters.js',
+  'src/formatters.js',
+  'src/validators.js',
+  'src/services.js',
+  'src/directives.js',
+  'src/markups.js',
+  'src/widgets.js',
+  'src/AngularPublic.js',
+]
+
+ANGULAR_SCENARIO = [
+  'src/scenario/Scenario.js',
+  'src/scenario/Application.js',
+  'src/scenario/Describe.js',
+  'src/scenario/Future.js',
+  'src/scenario/HtmlUI.js',
+  'src/scenario/Describe.js',
+  'src/scenario/Runner.js',
+  'src/scenario/SpecRunner.js',
+  'src/scenario/dsl.js',
+  'src/scenario/matchers.js',
+]
+
+GENERATED_FILES = [
+  'angular-debug.js',
+  'angular-minified.js',
+  'angular-minified.map',
+  'angular-scenario.js',
+]
+
 task :default => [:compile, :test]
 
 desc 'Generate Externs'
@@ -20,31 +61,27 @@ task :compile_externs do
   out.close
 end
 
+desc 'Clean Generated Files'
+task :clean do  
+  GENERATED_FILES.each do |file|
+    `rm #{file}`
+  end
+end
+
 desc 'Compile Scenario'
 task :compile_scenario do
-  concat = %x(cat \
-      lib/jquery/jquery-1.4.2.js \
-      src/scenario/angular.prefix \
-      src/Angular.js \
-      src/jqLite.js \
-      src/JSON.js \
-      src/Scope.js \
-      src/Injector.js \
-      src/Parser.js \
-      src/Resource.js \
-      src/Browser.js \
-      src/apis.js \
-      src/services.js \
-      src/AngularPublic.js \
-      src/scenario/DSL.js \
-      src/scenario/Future.js \
-      src/scenario/Matcher.js \
-      src/scenario/Runner.js \
-      src/scenario/angular.suffix \
-    )
+  
+  deps = [
+      'lib/jquery/jquery-1.4.2.js',
+      'src/scenario/angular.prefix',
+      ANGULAR,
+      ANGULAR_SCENARIO,
+      'src/scenario/angular.suffix',
+  ]
   css = %x(cat css/angular-scenario.css)
+  concat = 'cat ' + deps.flatten.join(' ')
   f = File.new("angular-scenario.js", 'w')
-  f.write(concat)
+  f.write(%x{#{concat}})
   f.write('document.write(\'\');')
@@ -54,30 +91,14 @@ end
 desc 'Compile JavaScript'
 task :compile => [:compile_externs, :compile_scenario] do
 
-  concat = %x(cat \
-      src/angular.prefix \
-      src/Angular.js \
-      src/JSON.js \
-      src/Compiler.js \
-      src/Scope.js \
-      src/Injector.js \
-      src/Parser.js \
-      src/Resource.js \
-      src/Browser.js \
-      src/jqLite.js \
-      src/apis.js \
-      src/filters.js \
-      src/formatters.js \
-      src/validators.js \
-      src/services.js \
-      src/directives.js \
-      src/markups.js \
-      src/widgets.js \
-      src/AngularPublic.js \
-      src/angular.suffix \
-    )
+  deps = [
+      'src/angular.prefix',
+      ANGULAR,
+      'src/angular.suffix',
+  ]
   f = File.new("angular-debug.js", 'w')
-  f.write(concat)
+  concat = 'cat ' + deps.flatten.join(' ')
+  f.write(%x{#{concat}})
   f.close
 
   %x(java -jar lib/compiler-closure/compiler.jar \
diff --git a/css/angular-scenario.css b/css/angular-scenario.css
index 3960c357..2cf24b19 100644
--- a/css/angular-scenario.css
+++ b/css/angular-scenario.css
@@ -1,76 +1,199 @@
 @charset "UTF-8";
 /* CSS Document */
 
-#runner {
-  position: absolute;
-  top:5px;
-  left:10px;
-  right:10px;
-  height: 200px;
+/** Structure */
+body {
+  font-family: Arial, sans-serif;
+  margin: 0;
+  font-size: 14px;
 }
 
-.console {
-  display: block;
-  overflow: scroll;
-  height: 200px;
-  border: 1px solid black;
+#header {
+  position: fixed;
+  width: 100%;
+}
+
+#specs {
+  padding-top: 50px;
+}
+
+#header .angular {
+  font-family: Courier New, monospace;
+  font-weight: bold;
+}
+
+#header h1 {
+  font-weight: normal;
+  float: left;
+  font-size: 30px;
+  line-height: 30px;
+  margin: 0;
+  padding: 10px 10px;
+  height: 30px;
+}
+
+#frame h2,
+#specs h2 {
+  margin: 0;
+  padding: 0.5em;
+  font-size: 1.1em;
+}
+      
+#status-legend {
+  margin-top: 10px;
+  margin-right: 10px;
+}
+
+#header, 
+#frame,
+.test-info, 
+.test-actions li {
+  overflow: hidden;
 }
 
-#testView {
-  position: absolute;
-  bottom:10px;
-  top:230px;
-  left:10px;
-  right:10px;
+#frame {
+  margin: 10px;
 }
 
-#testView iframe {
+#frame iframe {
   width: 100%;
-  height: 100%;
+  height: 758px;
+}
+
+#frame .popout {
+  float: right;
 }
 
-li.running > span {
-  background-color: yellow;
+#frame iframe {
+  border: none;
+}
+      
+.tests li, 
+.test-actions li,
+.test-it li, 
+.test-it ol,
+.status-display {
+  list-style-type: none;
 }
 
-#runner span {
-  background-color: green;
+.tests,
+.test-it ol,
+.status-display {
+  margin: 0;
+  padding: 0;
 }
 
-#runner .fail > span {
-  background-color: red;
+.test-info { 
+  margin-left: 1em;
+  margin-top: 0.5em;
+  border-radius: 8px 0 0 8px;
+  -webkit-border-radius: 8px 0 0 8px;
+  -moz-border-radius: 8px 0 0 8px;
+}
+                  
+.test-it ol {
+  margin-left: 2.5em;
 }
 
-.collapsed > ul {
-  display: none;
+.status-display,
+.status-display li {
+  float: right;
 }
 
-//////
+.status-display li {
+  padding: 5px 10px;
+}
 
-.run, .info, .error {
-  display: block;
-  padding: 0 1em;
+.timer-result, 
+.test-title {
+  display: inline-block;
+  margin: 0;
+  padding: 4px;
+}
+
+.timer-result {
+  width: 4em;
+  padding: 0 10px;
+  text-align: right;
   font-family: monospace;
-  white-space: pre;
 }
 
-.run {
-  background-color: lightgrey;
-  padding: 0 .2em;
+.test-it pre,
+.test-actions pre {
+  clear: left;
+  margin-left: 6em;
 }
 
-.run.pass {
-  background-color: lightgreen;
+.test-describe .test-describe {
+  margin: 5px 5px 10px 2em;
 }
 
-.run.fail {
-  background-color: lightred;
+.test-actions .status-pending .test-title:before {
+  content: 'ยป ';
+}
+
+/** Colors */
+
+#header {
+  background-color: #F2C200;
 }
 
-.name, .time, .state {
-  padding-right: 2em;
+#specs h2 {
+  border-top: 2px solid #BABAD1;
 }
 
-error {
-  color: red;
-}
\ No newline at end of file
+#specs h2,
+#frame h2 {
+  background-color: #efefef;
+}
+
+#frame {
+  border: 1px solid #BABAD1;
+}
+      
+.test-describe .test-describe {
+  border-left: 1px solid #BABAD1;
+  border-right: 1px solid #BABAD1;
+  border-bottom: 1px solid #BABAD1;
+}
+
+.status-display {
+  border: 1px solid #777;
+}
+
+.status-display .status-pending,
+.status-pending .test-info {
+  background-color: #F9EEBC;
+}
+
+.status-display .status-success,
+.status-success .test-info {
+  background-color: #B1D7A1; 
+}
+      
+.status-display .status-failure,
+.status-failure .test-info { 
+  background-color: #FF8286; 
+}
+
+.status-display .status-error,
+.status-error .test-info { 
+  background-color: black;
+  color: white;
+}
+
+.test-actions .status-success .test-title { 
+  color: #30B30A; 
+}
+
+.test-actions .status-failure .test-title { 
+  color: #DF0000;
+}
+
+.test-actions .status-error .test-title { 
+  color: black;
+}
+
+.test-actions .timer-result {
+  color: #888;
+}
diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf
index ed58d269..26bdd614 100644
--- a/jsTestDriver-jquery.conf
+++ b/jsTestDriver-jquery.conf
@@ -9,7 +9,7 @@ load:
   - src/JSON.js
   - src/*.js
   - test/testabilityPatch.js
-  - src/scenario/Runner.js
+  - src/scenario/Scenario.js
   - src/scenario/*.js
   - test/angular-mocks.js
   - test/scenario/*.js
diff --git a/jsTestDriver.conf b/jsTestDriver.conf
index c7d74b75..9d3d980d 100644
--- a/jsTestDriver.conf
+++ b/jsTestDriver.conf
@@ -9,7 +9,7 @@ load:
   - src/JSON.js
   - src/*.js
   - test/testabilityPatch.js
-  - src/scenario/Runner.js
+  - src/scenario/Scenario.js
   - src/scenario/*.js
   - test/angular-mocks.js
   - test/scenario/*.js
diff --git a/scenario/style.css b/scenario/style.css
index 956bdc52..43690e2c 100644
--- a/scenario/style.css
+++ b/scenario/style.css
@@ -5,3 +5,7 @@ th {
 tr {
   border: 1px solid black;
 }
+
+.redbox {
+  background-color: red;
+}
\ No newline at end of file
diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js
index f4488190..69fdc10e 100644
--- a/scenario/widgets-scenario.js
+++ b/scenario/widgets-scenario.js
@@ -1,25 +1,58 @@
-describe('widgets', function(){
+describe('widgets', function() {
   it('should verify that basic widgets work', function(){
-    browser.navigateTo('widgets.html');
-
-    expect('{{text.basic}}').toEqual('');
-    input('text.basic').enter('John');
-    expect('{{text.basic}}').toEqual('John');
-
-    expect('{{text.password}}').toEqual('');
+    navigateTo('widgets.html');
+    input('text.basic').enter('Carlos');
+    expect(binding('text.basic')).toEqual('Carlos');
+    pause(2);
+    input('text.basic').enter('Carlos Santana');
+    pause(2);
+    expect(binding('text.basic')).not().toEqual('Carlos Boozer');
+    pause(2);
     input('text.password').enter('secret');
-    expect('{{text.password}}').toEqual('secret');
-
-    expect('{{text.hidden}}').toEqual('hiddenValue');
-
-    expect('{{gender}}').toEqual('male');
+    expect(binding('text.password')).toEqual('secret');
+    expect(binding('text.hidden')).toEqual('hiddenValue');
+    expect(binding('gender')).toEqual('male');
+    pause(2);
     input('gender').select('female');
-    input('gender').isChecked('female');
-    expect('{{gender}}').toEqual('female');
-
-//    expect('{{tea}}').toBeChecked();
-//    input('gender').select('female');
-//    expect('{{gender}}').toEqual('female');
-
+    expect(binding('gender')).toEqual('female');
+    pause(2);
+  });
+  describe('do it again', function() {
+    it('should verify that basic widgets work', function(){
+      navigateTo('widgets.html');
+      input('text.basic').enter('Carlos');
+      expect(binding('text.basic')).toEqual('Carlos');
+      pause(2);
+      input('text.basic').enter('Carlos Santana');
+      pause(2);
+      expect(binding('text.basic')).toEqual('Carlos Santana');
+      pause(2);
+      input('text.password').enter('secret');
+      expect(binding('text.password')).toEqual('secret');
+      expect(binding('text.hidden')).toEqual('hiddenValue');
+      expect(binding('gender')).toEqual('male');
+      pause(2);
+      input('gender').select('female');
+      expect(binding('gender')).toEqual('female');
+      pause(2);
+    });
+  });
+  it('should verify that basic widgets work', function(){
+    navigateTo('widgets.html');
+    input('text.basic').enter('Carlos');
+    expect(binding('text.basic')).toEqual('Carlos');
+    pause(2);
+    input('text.basic').enter('Carlos Santana');
+    pause(2);
+    expect(binding('text.basic')).toEqual('Carlos Santana');
+    pause(2);
+    input('text.password').enter('secret');
+    expect(binding('text.password')).toEqual('secret');
+    expect(binding('text.hidden')).toEqual('hiddenValue');
+    expect(binding('gender')).toEqual('male');
+    pause(2);
+    input('gender').select('female');
+    expect(binding('gender')).toEqual('female');
+    pause(2);
   });
 });
diff --git a/src/Angular.js b/src/Angular.js
index 95970850..72f341f3 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -270,11 +270,11 @@ function equals(o1, o2) {
     } else {
       keySet = {};
       for(key in o1) {
-        if (key.charAt(0) !== '$' && !equals(o1[key], o2[key])) return false;
+        if (key.charAt(0) !== '$' && !isFunction(o1[key]) && !equals(o1[key], o2[key])) return false;
         keySet[key] = true;
       }
       for(key in o2) {
-        if (key.charAt(0) !== '$' && keySet[key] !== true) return false;
+        if (!keySet[key] && key.charAt(0) !== '$' && !isFunction(o2[key])) return false;
       }
       return true;
     }
diff --git a/src/scenario/Application.js b/src/scenario/Application.js
new file mode 100644
index 00000000..24ae99e9
--- /dev/null
+++ b/src/scenario/Application.js
@@ -0,0 +1,51 @@
+/**
+ * Represents the application currently being tested and abstracts usage
+ * of iframes or separate windows.
+ */
+angular.scenario.Application = function(context) {
+  this.context = context;
+  context.append('
Current URL: None
');
+};
+
+/**
+ * Gets the jQuery collection of frames. Don't use this directly because
+ * frames may go stale.
+ *
+ * @return {Object} jQuery collection
+ */
+angular.scenario.Application.prototype.getFrame = function() {
+  return this.context.find('> iframe');
+};
+
+/**
+ * Gets the window of the test runner frame. Always favor executeAction() 
+ * instead of this method since it prevents you from getting a stale window.
+ *
+ * @return {Object} the window of the frame
+ */
+angular.scenario.Application.prototype.getWindow = function() {
+  var contentWindow = this.getFrame().attr('contentWindow');
+  if (!contentWindow)
+    throw 'No window available because frame not loaded.';
+  return contentWindow;
+};
+
+/**
+ * Changes the location of the frame.
+ */
+angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
+  this.getFrame().remove();
+  this.context.append('');
+  this.context.find('> h2 a').attr('href', url).text(url);
+  this.getFrame().attr('src', url).load(onloadFn);
+};
+
+/**
+ * Executes a function in the context of the tested application.
+ *
+ * @param {Function} The callback to execute. function($window, $document)
+ */
+angular.scenario.Application.prototype.executeAction = function(action) {
+  var $window = this.getWindow();
+  return action.call($window, _jQuery($window.document), $window);
+};
diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js
index dc85ea45..a7571afe 100644
--- a/src/scenario/DSL.js
+++ b/src/scenario/DSL.js
@@ -1,131 +1,134 @@
-angular.scenario.dsl.browser = {
-  navigateTo: function(url){
-    var location = this.location;
-    return $scenario.addFuture('Navigate to: ' + url, function(done){
-      var self = this;
-      this.testFrame.load(function(){
-        self.testFrame.unbind();
-        self.testWindow = self.testFrame[0].contentWindow;
-        self.testDocument = self.jQuery(self.testWindow.document);
-        self.$browser = self.testWindow.angular.service.$browser();
-        self.notifyWhenNoOutstandingRequests =
-          bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests);
-        self.notifyWhenNoOutstandingRequests(done);
-      });
-      if (this.testFrame.attr('src') == url) {
-        this.testFrame[0].contentWindow.location.reload();
-      } else {
-        this.testFrame.attr('src', url);
-        location.setLocation(url);
-      }
-    });
-  },
-  location: {
-    href: "",
-    hash: "",
-    toEqual: function(url) {
-      return (this.hash === "" ? (url == this.href) :
-        (url == (this.href + "/#/" + this.hash)));
-    },
-    setLocation: function(url) {
-      var urlParts = url.split("/#/");
-      this.href = urlParts[0] || "";
-      this.hash = urlParts[1] || "";
-    }
-  }
-};
-
-angular.scenario.dsl.input = function(selector) {
-  var namePrefix = "input '" + selector + "'";
-  return {
-    enter: function(value) {
-      return $scenario.addFuture(namePrefix + " enter '" + value + "'", function(done) {
-        var input = this.testDocument.find('input[name=' + selector + ']');
-        input.val(value);
-        this.testWindow.angular.element(input[0]).trigger('change');
-        done();
-      });
-    },
-    select: function(value) {
-      return $scenario.addFuture(namePrefix + " select '" + value + "'", function(done) {
-        var input = this.testDocument.
-          find(':radio[name$=@' + selector + '][value=' + value + ']');
-        jqLiteWrap(input[0]).trigger('click');
-        input[0].checked = true;
-        done();
-      });
-    }
-  };
-};
+/**
+ * Shared DSL statements that are useful to all scenarios.
+ */
 
-angular.scenario.dsl.NG_BIND_PATTERN =/\{\{[^\}]+\}\}/;
+/**
+* Usage:
+*    pause(seconds) pauses the test for specified number of seconds
+*/
+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);
+   });
+ };
+});
 
-angular.scenario.dsl.repeater = function(selector) {
-  var namePrefix = "repeater '" + selector + "'";
-  return {
-    count: function() {
-      return $scenario.addFuture(namePrefix + ' count', function(done) {
-          done(this.testDocument.find(selector).size());
-      });
-    },
-    collect: function(collectSelector) {
-      return $scenario.addFuture(
-          namePrefix + " collect '" + collectSelector + "'",
-          function(done) {
-        var self = this;
-        var doCollect = bind(this, function() {
-          var repeaterArray = [], ngBindPattern;
-          var startIndex = collectSelector.search(
-              angular.scenario.dsl.NG_BIND_PATTERN);
-          if (startIndex >= 0) {
-            ngBindPattern = collectSelector.substring(
-                startIndex + 2, collectSelector.length - 2);
-            collectSelector = '*';
-            
-          }
-          this.testDocument.find(selector).each(function() {
-            var element = self.jQuery(this);
-            element.find(collectSelector).
-              each(function() {
-                var foundElem = self.jQuery(this);
-                if (foundElem.attr('ng:bind') == ngBindPattern) {
-                  repeaterArray.push(foundElem.text());
-                }
-            });
-          });
-          return repeaterArray;
-        });
-        done(doCollect());
-      });
-    }
+/**
+ * Usage:
+ *    expect(future).{matcher} where matcher is one of the matchers defined
+ *    with angular.scenario.matcher
+ *
+ * ex. expect(binding("name")).toEqual("Elliott")
+ */
+angular.scenario.dsl('expect', function() {
+  var chain = angular.extend({}, angular.scenario.matcher);
+  
+  chain.not = function() {
+    this.inverse = true;
+    return chain;
+  };
+  
+  return function(future) {
+    this.future = future;
+    return chain;
   };
-};
+});
 
-angular.scenario.dsl.element = function(selector) {
-  var namePrefix = "Element '" + selector + "'";
-  var futureJquery = {};
-  for (key in (jQuery || _jQuery).fn) {
-    (function(){
-      var jqFnName = key;
-      var jqFn = (jQuery || _jQuery).fn[key];
-      futureJquery[key] = function() {
-        var jqArgs = arguments;
-        return $scenario.addFuture(namePrefix + "." + jqFnName + "()",
-                function(done) {
-          var self = this, repeaterArray = [], ngBindPattern;
-          var startIndex = selector.search(angular.scenario.dsl.NG_BIND_PATTERN);
-          if (startIndex >= 0) {
-            ngBindPattern = selector.substring(startIndex + 2, selector.length - 2);
-            var element = this.testDocument.find('*').filter(function() {
-              return self.jQuery(this).attr('ng:bind') == ngBindPattern;
+/**
+ * Usage:
+ *    navigateTo(future|string) where url a string or future with a value 
+ *    of a  URL to navigate to
+ */
+angular.scenario.dsl('navigateTo', function() {
+  return function(url) {
+    var application = this.application;
+    var name = url;
+    if (url.name) {
+      name = ' value of ' + url.name;
+    }
+    return this.addFuture('navigate to ' + name, function(done) {
+      application.navigateTo(url.value || url, function() {
+        application.executeAction(function() {
+          if (this.angular) {
+            var $browser = this.angular.service.$browser();
+            $browser.poll();
+            $browser.notifyWhenNoOutstandingRequests(function() {
+              done(null, url.value || url);
             });
-            done(jqFn.apply(element, jqArgs));
           } else {
-            done(jqFn.apply(this.testDocument.find(selector), jqArgs));
+            done(null, url.value || url);
           }
         });
-      };
-    })();
-  }
-  return futureJquery;
-};
+      });
+    });
+  };
+});
+
+/**
+ * Usage:
+ *    input(name).enter(value) enters value in input with specified name
+ *    input(name).check() checks checkbox
+ *    input(name).select(value) selects the readio button with specified name/value
+ */
+angular.scenario.dsl('input', function() {
+  var chain = {};
+  
+  chain.enter = function(value) {
+    var spec = this;
+    return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function(done) {
+      var input = _jQuery(this.document).find('input[name=' + spec.name + ']');
+      if (!input.length)
+        return done("Input named '" + spec.name + "' does not exist.");
+      input.val(value);
+      this.angular.element(input[0]).trigger('change');
+      done();
+    });
+  };
+  
+  chain.check = function() {
+    var spec = this;
+    return this.addFutureAction("checkbox '" + this.name + "' toggle", function(done) {
+      var input = _jQuery(this.document).
+        find('input:checkbox[name=' + spec.name + ']');
+      if (!input.length)
+        return done("Input named '" + spec.name + "' does not exist.");
+      this.angular.element(input[0]).trigger('click');
+      input.attr('checked', !input.attr('checked'));
+      done();
+    });
+  };
+  
+  chain.select = function(value) {
+    var spec = this;
+    return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function(done) {
+      var input = _jQuery(this.document).
+        find('input:radio[name$="@' + spec.name + '"][value="' + value + '"]');
+      if (!input.length)
+        return done("Input named '" + spec.name + "' does not exist.");
+      this.angular.element(input[0]).trigger('click');
+      input.attr('checked', !input.attr('checked'));
+      done();
+    });
+  };
+  
+  return function(name) {
+    this.name = name;
+    return chain;
+  };
+});
+
+/**
+ * Usage:
+ *    binding(name) returns the value of a binding
+ */
+angular.scenario.dsl('binding', function() {
+  return function(name) {
+    return this.addFutureAction("select binding '" + name + "'", function(done) {
+      var element = _jQuery(this.document).find('[ng\\:bind="' + name + '"]');
+      if (!element.length)
+        return done("Binding named '" + name + "' does not exist.");
+      done(null, element.text());
+    });
+  };
+});
diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js
new file mode 100644
index 00000000..896b337f
--- /dev/null
+++ b/src/scenario/Describe.js
@@ -0,0 +1,108 @@
+/**
+ * The representation of define blocks. Don't used directly, instead use
+ * define() in your tests.
+ */
+angular.scenario.Describe = function(descName, parent) {
+  this.beforeEachFns = [];
+  this.afterEachFns = [];
+  this.its = [];
+  this.children = [];
+  this.name = descName;
+  this.parent = parent;
+  this.id = angular.scenario.Describe.id++;
+  
+  /**
+   * Calls all before functions.
+   */
+  var beforeEachFns = this.beforeEachFns;
+  this.setupBefore = function() {
+    if (parent) parent.setupBefore.call(this);
+    angular.foreach(beforeEachFns, function(fn) { fn.call(this); }, this);
+  };
+
+  /**
+   * Calls all after functions.
+   */
+  var afterEachFns = this.afterEachFns;
+  this.setupAfter  = function() {
+    angular.foreach(afterEachFns, function(fn) { fn.call(this); }, this);
+    if (parent) parent.setupAfter.call(this);
+  };
+};
+
+// Shared Unique ID generator for every describe block
+angular.scenario.Describe.id = 0;
+
+/**
+ * Defines a block to execute before each it or nested describe.
+ *
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.beforeEach = function(body) {
+  this.beforeEachFns.push(body);
+};
+
+/**
+ * Defines a block to execute after each it or nested describe.
+ *
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.afterEach = function(body) {
+  this.afterEachFns.push(body);
+};
+
+/**
+ * Creates a new describe block that's a child of this one.
+ *
+ * @param {String} Name of the block. Appended to the parent block's name.
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.describe = function(name, body) {
+  var child = new angular.scenario.Describe(name, this);
+  this.children.push(child);
+  body.call(child);
+};
+
+/**
+ * Use to disable a describe block.
+ */
+angular.scenario.Describe.prototype.xdescribe = angular.noop;
+
+/**
+ * Defines a test.
+ *
+ * @param {String} Name of the test.
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.it = function(name, body) {
+  var self = this;
+  this.its.push({
+    definition: this,
+    name: name, 
+    fn: function() {
+      self.setupBefore.call(this);
+      body.call(this);
+      self.setupAfter.call(this);
+    }
+  });
+};
+
+/**
+ * Use to disable a test block.
+ */
+angular.scenario.Describe.prototype.xit = angular.noop;
+
+/**
+ * Gets an array of functions representing all the tests (recursively).
+ * that can be executed with SpecRunner's.
+ */
+angular.scenario.Describe.prototype.getSpecs = function() {
+  var specs = arguments[0] || [];
+  angular.foreach(this.children, function(child) {
+    child.getSpecs(specs);
+  });
+  angular.foreach(this.its, function(it) {
+    specs.push(it);
+  });
+  return specs;
+};
diff --git a/src/scenario/Future.js b/src/scenario/Future.js
index cc40eff0..60fad9c5 100644
--- a/src/scenario/Future.js
+++ b/src/scenario/Future.js
@@ -1,13 +1,22 @@
-function Future(name, behavior) {
+/**
+ * A future action in a spec.
+ */
+angular.scenario.Future = function(name, behavior) {
   this.name = name;
   this.behavior = behavior;
   this.fulfilled = false;
-  this.value = _undefined;
-}
+  this.value = undefined;
+};
 
-Future.prototype = {
-  fulfill: function(value) {
+/**
+ * Executes the behavior of the closure.
+ *
+ * @param {Function} Callback function(error, result)
+ */
+angular.scenario.Future.prototype.execute = function(doneFn) {
+  this.behavior(angular.bind(this, function(error, result) {
     this.fulfilled = true;
-    this.value = value;
-  }
+    this.value = error || result;
+    doneFn(error, result);
+  }));
 };
diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js
new file mode 100644
index 00000000..46c88837
--- /dev/null
+++ b/src/scenario/HtmlUI.js
@@ -0,0 +1,204 @@
+/**
+ * User Interface for the Scenario Runner.
+ *
+ * @param {Object} The jQuery UI object for the UI.
+ */
+angular.scenario.ui.Html = function(context) {
+  this.context = context;
+  context.append(
+    '' +
+    ''
+  );
+};
+
+/**
+ * Adds a new spec to the UI.
+ *
+ * @param {Object} The spec object created by the Describe object.
+ */
+angular.scenario.ui.Html.prototype.addSpec = function(spec) {
+  var specContext = this.findContext(spec.definition);
+  specContext.find('> .tests').append(
+    ''
+  );
+  specContext = specContext.find('> .tests li:last');
+  return new angular.scenario.ui.Html.Spec(specContext, spec.name, 
+    angular.bind(this, function(status) {
+      var status = this.context.find('#status-legend .status-' + status);
+      var parts = status.text().split(' ');
+      var value = (parts[0] * 1) + 1;
+      status.text(value + ' ' + parts[1]);
+    })
+  );
+};
+
+/**
+ * Finds the context of a spec block defined by the passed definition.
+ *
+ * @param {Object} The definition created by the Describe object.
+ */
+angular.scenario.ui.Html.prototype.findContext = function(definition) {
+  var path = [];
+  var currentContext = this.context.find('#specs');
+  var currentDefinition = definition;
+  while (currentDefinition && currentDefinition.name) {
+    path.unshift(currentDefinition);
+    currentDefinition = currentDefinition.parent;
+  }
+  angular.foreach(path, angular.bind(this, function(defn) {
+    var id = 'describe-' + defn.id;
+    if (!this.context.find('#' + id).length) {
+      currentContext.find('> .test-children').append(
+        '' +
+        '  
' +
+        '  
' +
+        '  
' +
+        '
' +
+    '  
' +
+    '    ' +
+    '    ' +
+    '  
' +
+    '
' +
+    '
'    
+  );
+  context.find('> .test-info .test-name').text('it ' + name);
+};
+
+/**
+ * Adds a new Step to this spec and returns it.
+ *
+ * @param {String} The name of the step.
+ */
+angular.scenario.ui.Html.Spec.prototype.addStep = function(name) {
+  this.context.find('> .test-actions').append('');
+  var stepContext = this.context.find('> .test-actions li:last');
+  var self = this;
+  return new angular.scenario.ui.Html.Step(stepContext, name, function(status) {
+    self.status = status;
+  });
+};
+
+/**
+ * Completes the spec and sets the timer value.
+ */
+angular.scenario.ui.Html.Spec.prototype.complete = function() {
+  this.context.removeClass('status-pending');
+  var endTime = new Date().getTime();
+  this.context.find("> .test-info .timer-result")
+    .text((endTime - this.startTime) + "ms");
+};
+
+/**
+ * Finishes the spec, possibly with an error.
+ *
+ * @param {Object} An optional error
+ */
+angular.scenario.ui.Html.Spec.prototype.finish = function(error) {
+  this.complete();
+  if (error) {
+    if (this.status !== 'failure') {
+      this.status = 'error';
+    }
+    this.context.append('');
+    this.context.find('pre:first').text(error.stack || error.toString());
+  }
+  this.context.addClass('status-' + this.status);
+  this.doneFn(this.status);
+};
+
+/**
+ * Finishes the spec, but with a Fatal Error.
+ *
+ * @param {Object} Required error
+ */
+angular.scenario.ui.Html.Spec.prototype.error = function(error) {
+  this.finish(error);
+};
+
+/**
+ * A single step inside an it block (or a before/after function).
+ *
+ * @param {Object} The jQuery object for the context of the step.
+ * @param {String} The name of the step.
+ * @param {Function} Callback function(status) to call when complete.
+ */
+angular.scenario.ui.Html.Step = function(context, name, doneFn) {
+  this.context = context;
+  this.name = name;
+  this.startTime = new Date().getTime();
+  this.doneFn = doneFn;
+  context.append(
+    '' +
+    ''
+  );
+  context.find('> .test-title').text(name);
+};
+
+/**
+ * Completes the step and sets the timer value.
+ */
+angular.scenario.ui.Html.Step.prototype.complete = function() {
+  this.context.removeClass('status-pending');
+  var endTime = new Date().getTime();
+  this.context.find(".timer-result")
+    .text((endTime - this.startTime) + "ms");
+};
+
+/**
+ * Finishes the step, possibly with an error.
+ *
+ * @param {Object} An optional error
+ */
+angular.scenario.ui.Html.Step.prototype.finish = function(error) {
+  this.complete();
+  if (error) {
+    this.context.addClass('status-failure');
+    this.doneFn('failure');
+  } else {
+    this.context.addClass('status-success');
+    this.doneFn('success');
+  }
+};
+
+/**
+ * Finishes the step, but with a Fatal Error.
+ *
+ * @param {Object} Required error
+ */
+angular.scenario.ui.Html.Step.prototype.error = function(error) {
+  this.complete();
+  this.context.addClass('status-error');
+  this.doneFn('error');
+};
diff --git a/src/scenario/Matcher.js b/src/scenario/Matcher.js
deleted file mode 100644
index a9c86571..00000000
--- a/src/scenario/Matcher.js
+++ /dev/null
@@ -1,21 +0,0 @@
-function Matcher(scope, future, logger) {
-  var self = scope.$scenario = this;
-  this.logger = logger;
-  this.future = future;
-}
-
-Matcher.addMatcher = function(name, matcher) {
-  Matcher.prototype[name] = function(expected) {
-    var future = this.future;
-    $scenario.addFuture(
-      'expect ' + future.name + ' ' + name + ' ' + expected,
-      function(done){
-        if (!matcher(future.value, expected))
-          throw "Expected " + expected + ' but was ' + future.value;
-        done();
-      }
-    );
-  };
-};
-
-Matcher.addMatcher('toEqual', angular.equals);
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index 77969618..0267bb2d 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -1,183 +1,95 @@
-angular['scenario'] = angular['scenario'] || (angular['scenario'] = {});
-angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {});
-
-angular.scenario.Runner = function(scope, jQuery){
-  var self = scope.$scenario = this;
-  this.scope = scope;
-  this.jQuery = jQuery;
-  this.scope.$testrun = {done: false, results: []};
-
-  var specs = this.specs = {};
-  this.currentSpec = {name: '', futures: []};
-  var path = [];
-  this.scope.describe = function(name, body){
-    path.push(name);
-    body();
-    path.pop();
-  };
-  var beforeEach = noop;
-  var afterEach = noop;
-  this.scope.beforeEach = function(body) {
-    beforeEach = body;
-  };
-  this.scope.afterEach = function(body) {
-    afterEach = body;
-  };
-  this.scope.expect = function(future) {
-    return new Matcher(self, future, self.logger);
+/**
+ * Runner for scenarios.
+ */
+angular.scenario.Runner = function($window) {
+  this.$window = $window;
+  this.rootDescribe = new angular.scenario.Describe();
+  this.currentDescribe = this.rootDescribe;
+  this.api = {
+    it: this.it,
+    xit: angular.noop,
+    describe: this.describe,
+    xdescribe: angular.noop,
+    beforeEach: this.beforeEach,
+    afterEach: this.afterEach
   };
-  this.scope.it = function(name, body) {
-    var specName = path.join(' ') + ': it ' + name;
-    self.currentSpec = specs[specName] = {
-        name: specName,
-        futures: []
-     };
+  angular.foreach(this.api, angular.bind(this, function(fn, key) {
+    this.$window[key] = angular.bind(this, fn);
+  }));
+};
+
+/**
+ * Defines a describe block of a spec.
+ *
+ * @param {String} Name of the block
+ * @param {Function} Body of the block
+ */
+angular.scenario.Runner.prototype.describe = function(name, body) {
+  var self = this;
+  this.currentDescribe.describe(name, function() {
+    var parentDescribe = self.currentDescribe;
+    self.currentDescribe = this;
     try {
-      beforeEach();
-      body();
-    } catch(err) {
-      self.addFuture(err.message || 'ERROR', function(){
-        throw err;
-      });
+      body.call(this);
     } finally {
-      afterEach();
+      self.currentDescribe = parentDescribe;
     }
-    self.currentSpec = _null;
-  };
-  this.logger = function returnNoop(){
-    return extend(returnNoop, {close:noop, fail:noop});
-  };
+  });
 };
 
-angular.scenario.Runner.prototype = {
-  run: function(body){
-    var jQuery = this.jQuery;
-    body.append(
-      '' +
-      '' +
-        '' +
-      '
');
-    var console = body.find('#runner .console');
-    console.find('li').live('click', function(){
-      jQuery(this).toggleClass('collapsed');
-    });
-    this.testFrame = body.find('#testView iframe');
-    function logger(parent) {
-      var container;
-      return function(type, text) {
-        if (!container) {
-          container = jQuery('');
-          parent.append(container);
-        }
-        var element = jQuery('');
-        element.find('span').text(text);
-        container.append(element);
-        return extend(logger(element), {
-          close: function(){
-            element.removeClass('running');
-            if(!element.hasClass('fail'))
-              element.addClass('collapsed');
-            console.scrollTop(console[0].scrollHeight);
-          },
-          fail: function(){
-            element.removeClass('running');
-            var current = element;
-            while (current[0] != console[0]) {
-              if (current.is('li'))
-                current.addClass('fail');
-              current = current.parent();
-            }
-          }
-        });
-      };
-    }
-    this.logger = logger(console);
-    var specNames = [];
-    foreach(this.specs, function(spec, name){
-      specNames.push(name);
-    }, this);
-    specNames.sort();
-    var self = this;
-    function callback(){
-      var next = specNames.shift();
-      if(next) {
-        self.execute(next, callback);
-      } else {
-        self.scope.$testrun.done = true;
-      }
-    }
-    callback();
-  },
+/**
+ * Defines a test in a describe block of a spec.
+ *
+ * @param {String} Name of the block
+ * @param {Function} Body of the block
+ */
+angular.scenario.Runner.prototype.it = function(name, body) { 
+  this.currentDescribe.it(name, body); 
+};
 
-  addFuture: function(name, behavior) {
-    var future = new Future(name, behavior);
-    this.currentSpec.futures.push(future);
-    return future;
-  },
+/**
+ * Defines a function to be called before each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.beforeEach = function(body) {
+  this.currentDescribe.beforeEach(body); 
+};
 
-  execute: function(name, callback) {
-   var spec = this.specs[name],
-       self = this,
-       futuresFulfilled = [],
-       result = {
-         passed: false,
-         failed: false,
-         finished: false,
-         fail: function(error) {
-           result.passed = false;
-           result.failed = true;
-           result.error = error;
-           result.log('fail', isString(error) ? error : toJson(error)).fail();
-         }
-       },
-       specThis = createScope({
-         result: result,
-         jQuery: this.jQuery,
-         testFrame: this.testFrame,
-         testWindow: this.testWindow
-       }, angularService, {});
-   this.self = specThis;
-   var futureLogger = this.logger('spec', name);
-   spec.nextFutureIndex = 0;
-   function done() {
-     result.finished = true;
-     futureLogger.close();
-     self.self = _null;
-     (callback||noop).call(specThis);
-   }
-   function next(value){
-     if (spec.nextFutureIndex > 0) {
-       spec.futures[spec.nextFutureIndex - 1].fulfill(value);
-     }
-     var future = spec.futures[spec.nextFutureIndex];
-     (result.log || {close:noop}).close();
-     result.log = _null;
-     if (future) {
-       spec.nextFutureIndex ++;
-       result.log = futureLogger('future', future.name);
-       futuresFulfilled.push(future.name);
-       try {
-         future.behavior.call(specThis, next);
-       } catch (e) {
-         console.error(e);
-         result.fail(e);
-         self.scope.$testrun.results.push(
-           {name: name, passed: false, error: e, steps: futuresFulfilled});
-         done();
-       }
-     } else {
-       result.passed = !result.failed;
-       self.scope.$testrun.results.push({
-         name: name,
-         passed: !result.failed,
-         error: result.error,
-         steps: futuresFulfilled});
-       done();
-     }
-   }
-   next();
-   return specThis;
-  }
-};
\ No newline at end of file
+/**
+ * Defines a function to be called after each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.afterEach = function(body) {
+  this.currentDescribe.afterEach(body); 
+};
+
+/**
+ * Defines a function to be called before each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
+  var $root = angular.scope({}, angular.service);
+  var self = this;
+  var specs = this.rootDescribe.getSpecs();
+  $root.application = application;
+  $root.ui = ui;
+  $root.setTimeout = function() { 
+    return self.$window.setTimeout.apply(self.$window, arguments);
+  };
+  asyncForEach(specs, angular.bind(this, function(spec, specDone) {
+    var runner = angular.scope($root);
+    runner.$become(specRunnerClass);
+    angular.foreach(angular.scenario.dsl, angular.bind(this, function(fn, key) {
+      this.$window[key] = function() {
+        return fn.call($root).apply(angular.scope(runner), arguments);
+      }
+    }));
+    runner.run(ui, spec, specDone);
+  }), specsDone || angular.noop);
+};
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
new file mode 100644
index 00000000..e93f6b2e
--- /dev/null
+++ b/src/scenario/Scenario.js
@@ -0,0 +1,103 @@
+/**
+ * Setup file for the Scenario. 
+ * Must be first in the compilation/bootstrap list.
+ */
+
+// Public namespace
+angular.scenario = {};
+
+// Namespace for the UI
+angular.scenario.ui = {};
+
+/**
+ * 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} The name of the statement
+ * @param {Function} Factory function(application), return a function for 
+ *  the statement.
+ */
+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] = angular.bind(self, 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} The name of the matcher
+ * @param {Function} The matching function(expected).
+ */
+angular.scenario.matcher = function(name, fn) {
+  angular.scenario.matcher[name] = function(expected) {
+    var prefix = 'expect ' + this.future.name + ' ';
+    if (this.inverse) {
+      prefix += 'not ';
+    }
+    this.addFuture(prefix + name + ' ' + angular.toJson(expected),
+      angular.bind(this, function(done) {
+        this.actual = this.future.value;
+        if ((this.inverse && fn.call(this, expected)) ||
+            (!this.inverse && !fn.call(this, expected))) {
+          this.error = 'expected ' + angular.toJson(expected) +
+            ' but was ' + angular.toJson(this.actual);
+        }
+        done(this.error);
+      })
+    );
+  };
+};
+
+/**
+ * Iterates through list with iterator function that must call the
+ * continueFunction to continute iterating.
+ *
+ * @param {Array} list to iterate over
+ * @param {Function} Callback function(value, continueFunction)
+ * @param {Function} Callback function(error, result) called when iteration 
+ *   finishes or an error occurs.
+ */
+function asyncForEach(list, iterator, done) {
+  var i = 0;
+  function loop(error) {
+    if (error || i >= list.length) {
+      done(error);
+    } else {
+      try {
+        iterator(list[i++], loop);
+      } catch (e) {
+        done(e);
+      }
+    }
+  }
+  loop();
+}
diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js
new file mode 100644
index 00000000..8b6d4ef1
--- /dev/null
+++ b/src/scenario/SpecRunner.js
@@ -0,0 +1,78 @@
+/**
+ * This class is the "this" of the it/beforeEach/afterEach method.
+ * Responsibilities:
+ *   - "this" for it/beforeEach/afterEach
+ *   - keep state for single it/beforeEach/afterEach execution
+ *   - keep track of all of the futures to execute
+ *   - run single spec (execute each future)
+ */
+angular.scenario.SpecRunner = function() {
+  this.futures = [];
+};
+
+/**
+ * Executes a spec which is an it block with associated before/after functions
+ * based on the describe nesting.
+ *
+ * @param {Object} An angular.scenario.UI implementation
+ * @param {Object} A spec object
+ * @param {Object} An angular.scenario.Application instance
+ * @param {Function} Callback function that is called when the  spec finshes.
+ */
+angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
+  var specUI = ui.addSpec(spec);
+  
+  try {
+    spec.fn.call(this);
+  } catch (e) {
+    specUI.error(e);
+    specDone();
+    return;
+  }
+  
+  asyncForEach(
+    this.futures, 
+    function(future, futureDone) {
+      var stepUI = specUI.addStep(future.name);
+      try {
+        future.execute(function(error) {
+          stepUI.finish(error);
+          futureDone(error);
+        });
+      } catch (e) {
+        stepUI.error(e);
+        rethrow(e);
+      }
+    }, 
+    function(e) {
+      specUI.finish(e); 
+      specDone(); 
+    }
+  );
+};
+
+/**
+ * Adds a new future action.
+ *
+ * @param {String} Name of the future
+ * @param {Function} Behavior of the future
+ */
+angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
+  var future = new angular.scenario.Future(name, angular.bind(this, behavior));
+  this.futures.push(future);
+  return future;
+};
+
+/**
+ * Adds a new future action to be executed on the application window.
+ *
+ * @param {String} Name of the future
+ * @param {Function} Behavior of the future
+ */
+angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) {
+  return this.addFuture(name, function(done) {
+    this.application.executeAction(function() {
+      behavior.call(this, done);
+    });
+  });
+};
diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix
index 5b44e17c..a1b4e151 100644
--- a/src/scenario/angular.prefix
+++ b/src/scenario/angular.prefix
@@ -22,9 +22,3 @@
  * THE SOFTWARE.
  */
 (function(window, document, previousOnLoad){
-  window.angular = {
-    scenario: {
-      dsl: window
-    }
-  };
-
diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix
index fc861cbf..53d99dd2 100644
--- a/src/scenario/angular.suffix
+++ b/src/scenario/angular.suffix
@@ -1,11 +1,31 @@
+  var $scenario = new angular.scenario.Runner(window);
 
-  var $scenarioRunner = new angular.scenario.Runner(window, jQuery);
-
-  window.onload = function(){
+  window.onload = function() {
     try {
       if (previousOnLoad) previousOnLoad();
     } catch(e) {}
-    $scenarioRunner.run(jQuery(window.document.body));
+    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, function(error) {
+      frame.remove();
+      if (error) {
+        if (window.console) {
+          console.log(error);
+          if (error.stack) {
+            console.log(error.stack);
+          }
+        } else {
+          // Do something for IE
+          alert(error);
+        }
+      }
+    });
   };
 
 })(window, document, window.onload);
diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js
index f74305c3..014c636d 100644
--- a/src/scenario/bootstrap.js
+++ b/src/scenario/bootstrap.js
@@ -1,4 +1,4 @@
-(function(onLoadDelegate){
+(function(previousOnLoad){
   var prefix = (function(){
     var filename = /(.*\/)bootstrap.js(#(.*))?/;
     var scripts = document.getElementsByTagName("script");
@@ -10,6 +10,7 @@
       }
     }
   })();
+  
   function addScript(path) {
     document.write('');
   }
@@ -18,26 +19,51 @@
     document.write('');
   }
 
-  window.angular = {
-    scenario: {
-      dsl: window
-    }
-  };
-
   window.onload = function(){
-    setTimeout(function(){
-      $scenarioRunner.run(jQuery(window.document.body));
-    }, 0);
-    (onLoadDelegate||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");
+  addScript("../angular-bootstrap.js");
+
+  addScript("Scenario.js");
+  addScript("Application.js");
+  addScript("Describe.js");
+  addScript("Future.js");
+  addScript("HtmlUI.js");
   addScript("Runner.js");
-  addScript("../Angular.js");
-  addScript("../JSON.js");
-  addScript("DSL.js");
-  document.write('');
-})(window.onload);
+  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/matchers.js b/src/scenario/matchers.js
new file mode 100644
index 00000000..0dfbc455
--- /dev/null
+++ b/src/scenario/matchers.js
@@ -0,0 +1,39 @@
+/**
+ * Matchers for implementing specs. Follows the Jasmine spec conventions.
+ */
+
+angular.scenario.matcher('toEqual', function(expected) {
+  return angular.equals(this.actual, expected);
+});
+
+angular.scenario.matcher('toBeDefined', function() {
+  return angular.isDefined(this.actual);
+});
+
+angular.scenario.matcher('toBeTruthy', function() {
+  return this.actual;
+});
+
+angular.scenario.matcher('toBeFalsy', function() {
+  return !this.actual;
+});
+
+angular.scenario.matcher('toMatch', function(expected) {
+  return new RegExp(expected).test(this.actual);
+});
+
+angular.scenario.matcher('toBeNull', function() {
+  return this.actual === null;
+});
+
+angular.scenario.matcher('toContain', function(expected) {
+  return includes(this.actual, expected);
+});
+
+angular.scenario.matcher('toBeLessThan', function(expected) {
+  return this.actual < expected;
+});
+
+angular.scenario.matcher('toBeGreaterThan', function(expected) {
+  return this.actual > expected;
+});
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index e0228e16..6faed707 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -86,6 +86,10 @@ describe('equals', function(){
     expect(equals({name:'misko'}, {name:'misko', $id:2})).toEqual(true);
     expect(equals({name:'misko', $id:1}, {name:'misko'})).toEqual(true);
   });
+
+  it('should ignore functions', function(){
+    expect(equals({func: function() {}}, {bar: function() {}})).toEqual(true);
+  });
 });
 
 describe('parseKeyValue', function() {
diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js
new file mode 100644
index 00000000..706fbc36
--- /dev/null
+++ b/test/scenario/ApplicationSpec.js
@@ -0,0 +1,75 @@
+describe('angular.scenario.Application', function() {
+  var app, frames;
+
+  beforeEach(function() {
+    frames = _jQuery("");
+    app = new angular.scenario.Application(frames);
+  });
+
+  it('should return new $window and $document after navigate', function() {
+    var testWindow, testDocument, counter = 0;
+    app.navigateTo = noop;
+    app.getWindow = function() { 
+      return {x:counter++, document:{x:counter++}}; 
+    };
+    app.navigateTo('http://www.google.com/');
+    app.executeAction(function($document, $window) {
+      testWindow = $window;
+      testDocument = $document;
+    });
+    app.navigateTo('http://www.google.com/');
+    app.executeAction(function($document, $window) {
+      expect($window).not.toEqual(testWindow);
+      expect($document).not.toEqual(testDocument);
+    });
+  });
+
+  it('should execute callback on $window of frame', function() {
+    var testWindow = {document: {}};
+    app.getWindow = function() { 
+      return testWindow; 
+    };
+    app.executeAction(function($document, $window) {
+      expect(this).toEqual($window);
+      expect(this).toEqual(testWindow);
+    });
+  });
+  
+  it('should create a new iframe each time', function() {
+    app.navigateTo('about:blank');
+    var frame = app.getFrame();
+    frame.attr('test', true);
+    app.navigateTo('about:blank');
+    expect(app.getFrame().attr('test')).toBeFalsy();
+  });
+  
+  it('should URL description bar', function() {
+    app.navigateTo('about:blank');
+    var anchor = frames.find('> h2 a');
+    expect(anchor.attr('href')).toEqual('about:blank');
+    expect(anchor.text()).toEqual('about:blank');
+  });
+  
+  it('should call onload handler when frame loads', function() {
+    var called;
+    app.getFrame = function() { 
+      // Mock a little jQuery
+      var result = {
+        remove: function() { 
+          return result; 
+        },
+        attr: function(key, value) { 
+          return (!value) ? 'attribute value' : result;
+        },
+        load: function() { 
+          called = true; 
+        }
+      };
+      return result;
+    };
+    app.navigateTo('about:blank', function() {
+      called = true;
+    });
+    expect(called).toBeTruthy();
+  });
+});
diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js
index 7a8e2e3b..9b011847 100644
--- a/test/scenario/DSLSpec.js
+++ b/test/scenario/DSLSpec.js
@@ -1,181 +1,232 @@
-describe("DSL", function() {
+/**
+ * Very basic Mock of angular.
+ */
+function AngularMock() {
+  this.reset();
+  this.service = this;
+}
 
-  var lastDocument, executeFuture, Expect;
+AngularMock.prototype.reset = function() {
+  this.log = [];
+};
 
-  beforeEach(function() {
-    setUpContext();
-    executeFuture = function(future, html, callback) {
-      lastDocument = _jQuery('' + html + '
');
-      lastFrame = _jQuery('');
-      _jQuery(document.body).append(lastDocument);
-      var specThis = {
-        testWindow: window,
-        testDocument: lastDocument,
-        testFrame: lastFrame,
-        jQuery: _jQuery
-      };
-      future.behavior.call(specThis, callback || noop);
-    };
-    Expect = _window.expect;
-  });
-
-  describe("input", function() {
+AngularMock.prototype.element = function(node) {
+  this.log.push('element(' + node.nodeName.toLowerCase() + ')');
+  return this;
+};
 
-    var input = angular.scenario.dsl.input;
+AngularMock.prototype.trigger = function(value) {
+  this.log.push('element().trigger(' + value + ')');
+};
 
-    it('should enter', function() {
-      var future = input('name').enter('John');
-      expect(future.name).toEqual("input 'name' enter 'John'");
-      executeFuture(future, '');
-      expect(lastDocument.find('input').val()).toEqual('John');
-    });
+AngularMock.prototype.$browser = function() {
+  this.log.push('$brower()');
+  return this;
+};
 
-    it('should select', function() {
-      var future = input('gender').select('female');
-      expect(future.name).toEqual("input 'gender' select 'female'");
-      executeFuture(future,
-        '' +
-        '');
-      expect(lastDocument.find(':radio:checked').length).toEqual(1);
-      expect(lastDocument.find(':radio:checked').val()).toEqual('female');
-    });
-  });
+AngularMock.prototype.poll = function() {
+  this.log.push('$brower.poll()');
+  return this;
+};
 
-  describe('browser', function() {
-    var browser = angular.scenario.dsl.browser;
-    it('shoud return true if location with empty hash provided is same ' +
-        'as location of the page', function() {
-      browser.location.href = "http://server";
-      expect(browser.location.toEqual("http://server")).toEqual(true);
-    });
-    it('shoud return true if location with hash provided is same ' +
-        'as location of the page', function() {
-      browser.location.href = "http://server";
-      browser.location.hash = "hashPath";
-      expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
-    });
-    it('should return true if the location provided is the same as which ' +
-        'browser navigated to', function() {
-      var future = browser.navigateTo("http://server/#/hashPath");
-      expect(future.name).toEqual("Navigate to: http://server/#/hashPath");
-      executeFuture(future, '');
-      expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
-      expect(browser.location.toEqual("http://server/")).toEqual(false);
+AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) {
+  this.log.push('$brower.notifyWhenNoOutstandingRequests()');
+  fn();
+};
 
-      future = browser.navigateTo("http://server/");
-      expect(future.name).toEqual("Navigate to: http://server/");
-      executeFuture(future, '');
-      expect(browser.location.toEqual("http://server/")).toEqual(true);
-    });
+describe("angular.scenario.dsl", function() {
+  var $window;
+  var $root;
+  var application;
+  
+  beforeEach(function() {
+    $window = {
+      document: _jQuery(""),
+      angular: new AngularMock()
+    };
+    $root = angular.scope({}, angular.service);
+    $root.futures = [];
+    $root.addFuture = function(name, fn) {
+      this.futures.push(name);
+      fn.call(this, function(error, result) {
+        $root.futureError = error;
+        $root.futureResult = result;
+      });
+    };
+    $root.application = new angular.scenario.Application($window.document);
+    $root.application.getWindow = function() {
+      return $window; 
+    };
+    $root.application.navigateTo = function(url, callback) { 
+      $window.location = url;
+      callback();
+    };
+    // Just use the real one since it delegates to this.addFuture
+    $root.addFutureAction = angular.scenario.
+      SpecRunner.prototype.addFutureAction;
   });
-
-  describe('repeater', function() {
-
-    var repeater = angular.scenario.dsl.repeater;
-    var html;
+  
+  describe('Pause', function() {
     beforeEach(function() {
-      html = "" +
-          "" +
-            "| " +
-              "John Marston" +
-            "" +
-            " | " +
-              "Red Dead Redemption" +
-            "" +
-          " | 
" +
-          "" +
-            "| " +
-              "Nathan Drake" +
-            "" +
-            " | " +
-              "Uncharted" +
-            "" +
-          " | 
" +
-        "
";
+      $root.setTimeout = function(fn, value) {
+        $root.timerValue = value;
+        fn();
+      };
     });
-    it('should count', function() {
-      var future = repeater('.repeater-row').count();
-      expect(future.name).toEqual("repeater '.repeater-row' count");
-      executeFuture(future,
-        "a
" +
-        "b
",
-        function(value) {
-          future.fulfill(value);
-      });
-      expect(future.fulfilled).toBeTruthy();
-      expect(future.value).toEqual(2);
+    
+    it('should pause for specified seconds', function() {
+      angular.scenario.dsl.pause.call($root).call($root, 10);
+      expect($root.timerValue).toEqual(10000);
+      expect($root.futureResult).toEqual(10000);
+    });    
+  });
+  
+  describe('Expect', function() {
+    it('should chain and execute matcher', function() {
+      var future = {value: 10};
+      var result = angular.scenario.dsl.expect.call($root).call($root, future);
+      result.toEqual(10);
+      expect($root.futureError).toBeUndefined();
+      expect($root.futureResult).toBeUndefined();
+      var result = angular.scenario.dsl.expect.call($root).call($root, future);
+      result.toEqual(20);
+      expect($root.futureError).toBeDefined();
     });
-
-    function assertFutureState(future, expectedName, expectedValue) {
-      expect(future.name).toEqual(expectedName);
-      executeFuture(future, html, function(value) {
-        future.fulfill(value);
-      });
-      expect(future.fulfilled).toBeTruthy();
-      expect(future.value).toEqual(expectedValue);
-    }
-    it('should collect bindings', function() {
-      assertFutureState(repeater('.epic').collect('{{hero}}'),
-        "repeater '.epic' collect '{{hero}}'",
-        ['John Marston', 'Nathan Drake']);
-      assertFutureState(repeater('.epic').collect('{{game}}'),
-        "repeater '.epic' collect '{{game}}'",
-        ['Red Dead Redemption', 'Uncharted']);
+  });
+  
+  describe('NavigateTo', function() {    
+    it('should allow a string url', function() {
+      angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
+      expect($window.location).toEqual('http://myurl');
+      expect($root.futureResult).toEqual('http://myurl');
+    });
+    
+    it('should allow a future url', function() {
+      var future = {name: 'future name', value: 'http://myurl'};
+      angular.scenario.dsl.navigateTo.call($root).call($root, future);
+      expect($window.location).toEqual('http://myurl');
+      expect($root.futureResult).toEqual('http://myurl');
     });
-    it('should collect normal selectors', function() {
-      assertFutureState(repeater('.epic').collect('.hero-name'),
-        "repeater '.epic' collect '.hero-name'",
-        ['John Marston', 'Nathan Drake']);
-      assertFutureState(repeater('.epic').collect('.game-name'),
-        "repeater '.epic' collect '.game-name'",
-        ['Red Dead Redemption', 'Uncharted']);
+    
+    it('should complete if angular is missing from app frame', function() {
+      delete $window.angular;
+      angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
+      expect($window.location).toEqual('http://myurl');
+      expect($root.futureResult).toEqual('http://myurl');
     });
-    it('should collect normal attributes', function() {
-      //TODO(shyamseshadri) : Left as an exercise to the user
+    
+    it('should wait for angular notify when no requests pending', function() {
+      angular.scenario.dsl.navigateTo.call($root).call($root, 'url');
+      expect($window.angular.log).toContain('$brower.poll()');
+      expect($window.angular.log)
+        .toContain('$brower.notifyWhenNoOutstandingRequests()');
     });
   });
-
-  describe('element', function() {
-    var element = angular.scenario.dsl.element;
-    var html;
+  
+  describe('Element Finding', function() {
+    var doc;
+    //TODO(esprehn): Work around a bug in jQuery where attribute selectors
+    //  only work if they are executed on a real document, not an element.
+    //
+    //  ex. jQuery('#foo').find('[name="bar"]') // fails
+    //  ex. jQuery('#foo [name="bar"]') // works, wtf?
+    //
     beforeEach(function() {
-      html = '' +
-          '
' +
-            'Description : ' +
-              'Details...' +
-            '' +
-            'Date created: ' +
-              '01/01/01' +
-            '' +
-          '
' +
-        '