diff --git a/.externalToolBuilders/JSTD_Tests.launch b/.externalToolBuilders/JSTD_Tests.launch new file mode 100644 index 00000000..503cbaff --- /dev/null +++ b/.externalToolBuilders/JSTD_Tests.launch @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore index 2b10d121..d9989b4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +<<<<<<< HEAD .git4_perforce_config blaze-eclipse google3/blaze-* @@ -21,4 +22,12 @@ google3/.gwt-tmp google3/alloc google3tomcat google3/mbin -google3/mgenfiles \ No newline at end of file +google3/mgenfiles +======= +angular-minified.map +externs.js +angular.js +angular-minified.js +angular-debug.js +angular-scenario.js +>>>>>>> b129a1094e6b42ed82c3ccecc2f40daaa0a6cb6a diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..a7c382ed --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1 @@ +workspace.xml diff --git a/.idea/.rakeTasks b/.idea/.rakeTasks new file mode 100644 index 00000000..50fb6fec --- /dev/null +++ b/.idea/.rakeTasks @@ -0,0 +1,7 @@ + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..e206d70d --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/master.iml b/.idea/master.iml new file mode 100644 index 00000000..8f7472a8 --- /dev/null +++ b/.idea/master.iml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..bf08d02d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..12b24804 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..9d32e507 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 00000000..0fb4c323 --- /dev/null +++ b/.project @@ -0,0 +1,27 @@ + + + angular.js + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + org.eclipse.ui.externaltools.ExternalToolBuilder + auto,full,incremental, + + + LaunchConfigHandle + <project>/.externalToolBuilders/JSTD_Tests.launch + + + + + + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope new file mode 100644 index 00000000..7beec24e --- /dev/null +++ b/.settings/.jsdtscope @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 00000000..49c8cd4f --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.JRE_CONTAINER \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 00000000..11006e2a --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Global \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b6ad6d3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..28aadac1 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +Angular +====== + +Compiling +--------- + rake compile + +Running Tests +------------- + rake server:start + rake test + diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..c8de5b78 --- /dev/null +++ b/Rakefile @@ -0,0 +1,119 @@ +include FileUtils + +task :default => [:compile, :test] + +desc 'Generate Externs' +task :compile_externs do + out = File.new("externs.js", "w") + + out.write("function _(){};\n") + file = File.new("lib/underscore/underscore.js", "r") + while (line = file.gets) + if line =~ /^\s*_\.(\w+)\s*=.*$/ + out.write("_.#{$1}=function(){};\n") + end + end + file.close + + out.write("function jQuery(){};\n") + file = File.new("lib/jquery/jquery-1.4.2.js", "r") + while (line = file.gets) + if line =~ /^\s*(\w+)\s*:\s*function.*$/ + out.write("jQuery.#{$1}=function(){};\n") + end + end + file.close + out.write("jQuery.scope=function(){};\n") + out.write("jQuery.controller=function(){};\n") + + out.close +end + +desc 'Compile Scenario' +task :compile_scenario do + concat = %x(cat \ + lib/underscore/underscore.js \ + lib/jquery/jquery-1.4.2.js \ + src/scenario/angular.prefix \ + src/Angular.js \ + src/JSON.js \ + src/Scope.js \ + src/Parser.js \ + src/Resource.js \ + src/Browser.js \ + src/apis.js \ + src/services.js \ + src/AngularPublic.js \ + src/scenario/Runner.js \ + src/scenario/DSL.js \ + src/scenario/angular.suffix \ + ) + css = %x(cat css/angular-scenario.css) + f = File.new("angular-scenario.js", 'w') + f.write(concat) + f.write('document.write(\'\');') + f.close +end + +desc 'Compile JavaScript' +task :compile do + Rake::Task['compile_externs'].execute 0 + Rake::Task['compile_scenario'].execute 0 + + concat = %x(cat \ + src/angular.prefix \ + src/Angular.js \ + src/JSON.js \ + src/Compiler.js \ + src/Scope.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 \ + ) + f = File.new("angular-debug.js", 'w') + f.write(concat) + f.close + + %x(java -jar lib/compiler-closure/compiler.jar \ + --compilation_level ADVANCED_OPTIMIZATIONS \ + --js angular-debug.js \ + --externs externs.js \ + --create_source_map ./angular-minified.map \ + --js_output_file angular-minified.js) +end + +namespace :server do + desc 'Run JsTestDriver Server' + task :start do + sh %x(java -jar lib/jstestdriver/JsTestDriver.jar --browser open --port 9876) + end + + desc "Run JavaScript tests against the server" + task :test do + sh %(java -jar lib/jstestdriver/JsTestDriver.jar --tests all) + end +end + +desc "Run JavaScript tests" +task :test do + sh %(java -jar lib/jstestdriver/JsTestDriver.jar --tests all --browser open --port 9876) +end + +desc 'Lint' +task :lint do + out = %x(lib/jsl/jsl -conf lib/jsl/jsl.default.conf) + print out +end diff --git a/TODO.text b/TODO.text new file mode 100644 index 00000000..d4d013a5 --- /dev/null +++ b/TODO.text @@ -0,0 +1,6 @@ +* move angular-bootstrap.js out of anugular.js. +* 'angular' is the official namespace for public API + - angular.defaults = {} + - var scope = angular.compile(element, options); +* angular.js is not self boot straping by default. +* Remove SWFObject diff --git a/css/angular-scenario.css b/css/angular-scenario.css new file mode 100644 index 00000000..3960c357 --- /dev/null +++ b/css/angular-scenario.css @@ -0,0 +1,76 @@ +@charset "UTF-8"; +/* CSS Document */ + +#runner { + position: absolute; + top:5px; + left:10px; + right:10px; + height: 200px; +} + +.console { + display: block; + overflow: scroll; + height: 200px; + border: 1px solid black; +} + +#testView { + position: absolute; + bottom:10px; + top:230px; + left:10px; + right:10px; +} + +#testView iframe { + width: 100%; + height: 100%; +} + +li.running > span { + background-color: yellow; +} + +#runner span { + background-color: green; +} + +#runner .fail > span { + background-color: red; +} + +.collapsed > ul { + display: none; +} + +////// + +.run, .info, .error { + display: block; + padding: 0 1em; + font-family: monospace; + white-space: pre; +} + +.run { + background-color: lightgrey; + padding: 0 .2em; +} + +.run.pass { + background-color: lightgreen; +} + +.run.fail { + background-color: lightred; +} + +.name, .time, .state { + padding-right: 2em; +} + +error { + color: red; +} \ No newline at end of file diff --git a/css/angular.css b/css/angular.css new file mode 100644 index 00000000..0fb10cdf --- /dev/null +++ b/css/angular.css @@ -0,0 +1,189 @@ +@charset "UTF-8"; +/* CSS Document */ + +#ng-console { + border: thin solid black; + font-family: 'courier'; + font-size: x-small; +} + +#ng-console .ng-console-error { + color: red; +} + +#ng-console .ng-console-info { + color: blue; +} + +.ng-upload-widget object { + align:center; +} + +.ng-upload-widget a { + margin-right: .3em; +} + +.ng-upload-widget span { + color: #999999; + font-size: smaller; +} + +.ng-format-negative { + color: red; +} + +.ng-exception { + border: 2px solid #FF0000; + font-family: "Courier New", Courier, monospace; + font-size: smaller; +} + +.ng-validation-error { + border: 2px solid #FF0000; +} + +.ng-hidden { + display:none; +} + +/***************** + * DatePicker + *****************/ + +div.ui-widget { + font-size: 11px; + } + +/***************** + * OrderBy + *****************/ +.ng-ascend, +.ng-descend { + padding-right: 20px; + background-repeat: no-repeat; + background-position: right; +} +.ng-ascend { background-image: url(angular_images/arrow_ascend.png); } +.ng-descend { background-image: url(angular_images/arrow_descend.png); } + +/***************** + * TIP + *****************/ +#ng-callout { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 13px; + font-weight: normal; + font-family: Verdana, Arial, Helvetica, sans-serif; + vertical-align: baseline; + background: transparent; + text-decoration: none; +} + +#ng-callout .ng-arrow-left{ + background-image: url(angular_images/arrow_left.gif); + background-repeat: no-repeat; + background-position: left top; + position: absolute; + z-index:101; + left:-12px; + height:23px; + width:10px; + top:-3px; +} + +#ng-callout .ng-arrow-right{ + background-image: url(angular_images/arrow_right.gif); + background-repeat: no-repeat; + background-position: left top; + position: absolute; + z-index:101; + height:23px; + width:11px; + top:-2px; +} + +#ng-callout { + position: absolute; + z-index:100; + border: 2px solid #CCCCCC; + background-color: #fff; +} + +#ng-callout .ng-content{ + padding:10px 10px 10px 10px; + color:#333333; +} + + +#ng-callout .ng-title{ + background-color: #CCCCCC; + text-align: left; + padding-left: 8px; + padding-bottom: 5px; + padding-top: 2px; + font-weight:bold; +} + + +#ng-spacer { + height: 1.2em; +} + +#ng-loading { + position: fixed; + bottom: 0; + height: 1.2em; + width: 100%; + text-align: center; +} + +/***************** + * Login + *****************/ + +#ng-login { + z-index: 2000; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding-top: 100px; +} + +#ng-login .ng-login-container { + width: 500px; + height: 380px; + margin: auto; + border-top: 5px solid #FFF; + border-left: 5px solid #DDD; + border-right: 5px solid #777; + border-bottom: 5px solid #555; + padding: 0 3px 3px 0; +} + +#ng-login .ng-login-container iframe { + width: 100%; + height: 100%; + border: 2px solid black; +} + + +/***************** + * indicators + *****************/ +.ng-indicator-wait { + display: inline-block; + height: 16px; + width: 16px; + background-image: url("angular_images/indicator-wait.png"); +} + +.ng-input-indicator-wait { + background-image: url("angular_images/indicator-wait.png"); + background-position: right; + background-repeat: no-repeat; +} diff --git a/css/angular_images/arrow_ascend.png b/css/angular_images/arrow_ascend.png new file mode 100644 index 00000000..dd27b92b Binary files /dev/null and b/css/angular_images/arrow_ascend.png differ diff --git a/css/angular_images/arrow_descend.png b/css/angular_images/arrow_descend.png new file mode 100644 index 00000000..ec1cb5df Binary files /dev/null and b/css/angular_images/arrow_descend.png differ diff --git a/css/angular_images/arrow_left.gif b/css/angular_images/arrow_left.gif new file mode 100644 index 00000000..4c9e5c66 Binary files /dev/null and b/css/angular_images/arrow_left.gif differ diff --git a/css/angular_images/arrow_right.gif b/css/angular_images/arrow_right.gif new file mode 100644 index 00000000..3252c359 Binary files /dev/null and b/css/angular_images/arrow_right.gif differ diff --git a/css/angular_images/indicator-wait.png b/css/angular_images/indicator-wait.png new file mode 100644 index 00000000..5b33f7e5 Binary files /dev/null and b/css/angular_images/indicator-wait.png differ diff --git a/css/angular_images/loader-bar.gif b/css/angular_images/loader-bar.gif new file mode 100644 index 00000000..47adbf03 Binary files /dev/null and b/css/angular_images/loader-bar.gif differ diff --git a/example/calculator-bootstrap.html b/example/calculator-bootstrap.html new file mode 100644 index 00000000..c72837dc --- /dev/null +++ b/example/calculator-bootstrap.html @@ -0,0 +1,21 @@ + + + + + + + + + + Quantity: + * + Cost: + = {{a * b | currency}} + + diff --git a/example/calculator-minified_init.html b/example/calculator-minified_init.html new file mode 100644 index 00000000..4f113f87 --- /dev/null +++ b/example/calculator-minified_init.html @@ -0,0 +1,21 @@ + + + + + + + + + + Quantity: + * + Cost: + = {{a * b | currency}} + + diff --git a/example/calculator.html b/example/calculator.html new file mode 100644 index 00000000..43d013fc --- /dev/null +++ b/example/calculator.html @@ -0,0 +1,21 @@ + + + + + + + + + + Quantity: + * + Cost: + = {{a * b | currency}} + + diff --git a/example/index.html b/example/index.html new file mode 100644 index 00000000..12f88ccc --- /dev/null +++ b/example/index.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/example/memoryLeak.html b/example/memoryLeak.html new file mode 100644 index 00000000..9e5f512d --- /dev/null +++ b/example/memoryLeak.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + +
+ + diff --git a/example/temp.html b/example/temp.html new file mode 100644 index 00000000..d07a6948 --- /dev/null +++ b/example/temp.html @@ -0,0 +1,13 @@ + + + + + + + {{$location.hashSearch.order}}
+ A
+ B
+ C
+ {{$location.hashSearch.order}}
+ + diff --git a/example/tweeter/style.css b/example/tweeter/style.css new file mode 100644 index 00000000..e8468b6b --- /dev/null +++ b/example/tweeter/style.css @@ -0,0 +1,98 @@ +.loading {display: none;} +.fetching .loading {display: block;} + +a { + color: blue; +} + +h1 { + background-color: black; + margin: 0; + padding: .25em; + color: white; + border-bottom: 5px solid gray; +} + +.box { + border: 2px solid gray; +} + +.tweeter { + margin-right: 360px; +} + +ul { + list-style: none; + margin: 0; + padding: 0; +} + +li { + margin: .25em; + padding: 2px; +} + +li img { + float: left; + margin: 2px; + margin-right: .5em; + max-height: 48px; + min-height: 48px; +} + +li.even { + background-color: lightgray; +} + + +.addressbook { + float: right; + width: 350px; +} + +.addressbook li { + font-size: .9em; +} + +.clrleft { + clear: left; +} + +.notes { + font-size: .8em; + color: gray; +} + +.username, .nickname { + font-weight: bold; +} + +.editor { + padding: 4px; +} + +label { + color: gray; + display: inline-block; + width: 75px; + text-align: right; + padding: 2px; + margin-top: 10px; +} + +.editor input[type=text], +.editor textarea { + width: 230px; + vertical-align: text-top; +} + +.editor TEXTAREA { + height: 50px; +} + +.debug{ + font-size: .7em; + white-space: pre; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/example/tweeter/tweeter_addressbook.html b/example/tweeter/tweeter_addressbook.html new file mode 100644 index 00000000..4844c035 --- /dev/null +++ b/example/tweeter/tweeter_addressbook.html @@ -0,0 +1,80 @@ + + + + + + + + + + + +
+

Address Book

+ [ Filter: ] + +
+
+
+ + + + + + + + + + +
+
+
+
+mute={{mute|json}} + +userFilter={{userFilter|json}} + +tweetFilter={{tweetFilter|json}} + +$anchor={{$anchor}} + +users={{users}} + +tweets={{tweets}} +
+
+
+

Tweets: {{$anchor.user}}

+ [ Filter: + | << All + ] +
Loading...
+ +
+ + diff --git a/example/tweeter/tweeter_demo.html b/example/tweeter/tweeter_demo.html new file mode 100644 index 00000000..138d4e2b --- /dev/null +++ b/example/tweeter/tweeter_demo.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + (TODO: I should fetch current tweets) +
+

Tweets: {{$anchor.user}}

+ [ Filter: (TODO: this should act as search box) + | << All + ] +
Loading...
+ +
+
tweets=(TODO: display me!!!)
+ + diff --git a/example/tweeter/tweeterclient.js b/example/tweeter/tweeterclient.js new file mode 100644 index 00000000..84fc5ef7 --- /dev/null +++ b/example/tweeter/tweeterclient.js @@ -0,0 +1,36 @@ +function noop(){} +$(document).ready(function(){ + function xhr(method, url, data, callback){ + jQuery.getJSON(url, function(){ + callback.apply(this, arguments); + scope.updateView(); + }); + } + + var resourceFactory = new ResourceFactory(xhr); + + var Tweeter = resourceFactory.route("http://twitter.com/statuses/:service:username.json", {}, { + home: {method:'GET', params: {service:'home_timeline'}, isArray:true }, + user: {method:'GET', params: {service:'user_timeline/'}, isArray:true } + }); + + + var scope = window.scope = angular.compile(document, { + location:angular.startUrlWatcher() + }); + + function fetchTweets(username){ + return username ? Tweeter.user({username: username}) : Tweeter.home(); + } + + scope.set('fetchTweets', fetchTweets); + scope.set('users', [ + {screen_name:'mhevery', name:'Mi\u0161ko Hevery', + notes:'Author of http://www.getangular.com.', + profile_image_url:'http://a3.twimg.com/profile_images/54360179/Me_-_Small_Banner_normal.jpg'}, + {screen_name:'abrons', name:'Adam Abrons', + notes:'Author of & Ruby guru see: http://www.angularjs.org.', + profile_image_url:'http://media.linkedin.com/mpr/mpr/shrink_80_80/p/2/000/005/0a8/044278d.jpg'} + ]); + scope.init(); +}); diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf new file mode 100644 index 00000000..34538bce --- /dev/null +++ b/jsTestDriver-jquery.conf @@ -0,0 +1,24 @@ +server: http://localhost:9876 + +load: + - lib/jasmine/jasmine-0.10.3.js + - lib/jasmine-jstd-adapter/JasmineAdapter.js + - lib/jquery/jquery-1.4.2.js + - test/jquery_alias.js + - src/Angular.js + - src/*.js + - src/scenario/Runner.js + - src/scenario/*.js + - test/testabilityPatch.js + - test/angular-mocks.js + - test/scenario/*.js + - test/*.js + +exclude: + - src/angular.prefix + - src/angular.suffix + - src/angular-bootstrap.js + - src/AngularPublic.js + - src/scenario/bootstrap.js + - test/jquery_remove.js + diff --git a/jsTestDriver.conf b/jsTestDriver.conf new file mode 100644 index 00000000..16bcf1db --- /dev/null +++ b/jsTestDriver.conf @@ -0,0 +1,23 @@ +server: http://localhost:9876 + +load: + - lib/jasmine/jasmine-0.10.3.js + - lib/jasmine-jstd-adapter/JasmineAdapter.js + - lib/jquery/jquery-1.4.2.js + - test/jquery_remove.js + - src/Angular.js + - src/*.js + - src/scenario/Runner.js + - src/scenario/*.js + - test/testabilityPatch.js + - test/angular-mocks.js + - test/scenario/*.js + - test/*.js + +exclude: + - test/jquery_alias.js + - src/angular.prefix + - src/angular.suffix + - src/angular-bootstrap.js + - src/scenario/bootstrap.js + - src/AngularPublic.js diff --git a/jstd.log b/jstd.log new file mode 100644 index 00000000..950b6ce4 --- /dev/null +++ b/jstd.log @@ -0,0 +1,762 @@ +Apr 8, 2010 2:04:32 PM com.google.jstestdriver.ServerStartupAction run +INFO: Starting server... +Apr 8, 2010 2:04:32 PM org.mortbay.log.Slf4jLog info +INFO: Transparent ProxyServlet @ forward to http://localhost:9876 +Apr 8, 2010 2:04:33 PM com.google.jstestdriver.BrowserHunter captureBrowser +INFO: Browser Captured: Firefox version 3.6.3 (1) +Apr 8, 2010 2:04:33 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1?start HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:04:33 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:04:36 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1?start HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:04:43 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:04:53 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:04 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:14 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:24 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:34 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:44 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:05:54 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:04 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:14 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:24 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:34 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:44 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:06:54 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:07:04 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:07:14 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:07:24 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:07:34 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Pragma: no-cache +Cache-Control: no-cache +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 + + +Apr 8, 2010 2:07:44 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:07:55 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:05 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:15 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:25 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:35 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:45 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:08:55 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:05 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:15 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:25 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:35 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:45 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:09:55 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Pragma: no-cache +Cache-Control: no-cache +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 + + +Apr 8, 2010 2:10:05 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:15 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:25 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:35 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:45 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Content-Type: text/plain; charset=UTF-8 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:55 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1?start HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 +Pragma: no-cache +Cache-Control: no-cache + + +Apr 8, 2010 2:10:56 PM com.google.jstestdriver.BrowserQueryResponseServlet doPost +FINEST: POST: POST /query/1 HTTP/1.1 +Host: misko.i:9876 +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 +Accept: */* +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 115 +Connection: keep-alive +Referer: http://misko.i:9876/slave/1/Runnerstrict.html +Pragma: no-cache +Cache-Control: no-cache +Content-Type: text/plain; charset=UTF-8 +X-Requested-With: XMLHttpRequest +Content-Length: 0 + + diff --git a/lib/compiler-closure/COPYING b/lib/compiler-closure/COPYING new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/lib/compiler-closure/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/compiler-closure/README b/lib/compiler-closure/README new file mode 100644 index 00000000..af4e6106 --- /dev/null +++ b/lib/compiler-closure/README @@ -0,0 +1,193 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Contents +// + +The Closure Compiler performs checking, instrumentation, and +optimizations on JavaScript code. The purpose of this README is to +explain how to build and run the Closure Compiler. + +The Closure Compiler requires Java 6 or higher. +http://www.java.com/ + + +// +// Building The Closure Compiler +// + +There are three ways to get a Closure Compiler executable. + +1) Use one we built for you. + +Pre-built Closure binaries can be found at +http://code.google.com/p/closure-compiler/downloads/list + + +2) Check out the source and build it with Apache Ant. + +First, check out the full source tree of the Closure Compiler. There +are instructions on how to do this at the project site. +http://code.google.com/p/closure-compiler/source/checkout + +Apache Ant is a cross-platform build tool. +http://ant.apache.org/ + +At the root of the source tree, there is an Ant file named +build.xml. To use it, navigate to the same directory and type the +command + +ant jar + +This will produce a jar file called "build/compiler.jar". + + +3) Check out the source and build it with Eclipse. + +Eclipse is a cross-platform IDE. +http://www.eclipse.org/ + +Under Eclipse's File menu, click "New > Project ..." and create a +"Java Project." You will see an options screen. Give the project a +name, select "Create project from existing source," and choose the +root of the checked-out source tree as the existing directory. Verify +that you are using JRE version 6 or higher. + +Eclipse can use the build.xml file to discover rules. When you +navigate to the build.xml file, you will see all the build rules in +the "Outline" pane. Run the "jar" rule to build the compiler in +build/compiler.jar. + + +// +// Running The Closure Compiler +// + +Once you have the jar binary, running the Closure Compiler is straightforward. + +On the command line, type + +java -jar compiler.jar + +This starts the compiler in interactive mode. Type + +var x = 17 + 25; + +then hit "Enter", then hit "Ctrl-Z" (on Windows) or "Ctrl-D" (on Mac or Linux) +and "Enter" again. The Compiler will respond: + +var x=42; + +The Closure Compiler has many options for reading input from a file, +writing output to a file, checking your code, and running +optimizations. To learn more, type + +java -jar compiler.jar --help + +You can read more detailed documentation about the many flags at +http://code.google.com/closure/compiler/docs/gettingstarted_app.html + + +// +// Compiling Multiple Scripts +// + +If you have multiple scripts, you should compile them all together with +one compile command. + +java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js + +The Closure Compiler will concatenate the files in the order they're +passed at the command line. + +If you need to compile many, many scripts together, you may start to +run into problems with managing dependencies between scripts. You +should check out the Closure Library. It contains functions for +enforcing dependencies between scripts, and a tool called calcdeps.py +that knows how to give scripts to the Closure Compiler in the right +order. + +http://code.google.com/p/closure-library/ + +// +// Licensing +// + +Unless otherwise stated, all source files are licensed under +the Apache License, Version 2.0. + + +----- +Code under: +src/com/google/javascript/rhino +test/com/google/javascript/rhino + +URL: http://www.mozilla.org/rhino +Version: 1.5R3, with heavy modifications +License: Netscape Public License and MPL / GPL dual license + +Description: A partial copy of Mozilla Rhino. Mozilla Rhino is an +implementation of JavaScript for the JVM. The JavaScript parser and +the parse tree data structures were extracted and modified +significantly for use by Google's JavaScript compiler. + +Local Modifications: The packages have been renamespaced. All code not +relavant to parsing has been removed. A JSDoc parser and static typing +system have been added. + + +----- +Code in: +lib/libtrunk_rhino_parser_jarjared.jar + +URL: http://www.mozilla.org/rhino +Version: Trunk +License: Netscape Public License and MPL / GPL dual license + +Description: Mozilla Rhino is an implementation of JavaScript for the JVM. + +Local Modifications: None. We've used JarJar to renamespace the code +post-compilation. See: +http://code.google.com/p/jarjar/ + + +----- +Code in: +lib/google_common.jar + +URL: http://code.google.com/p/guava-libraries/ +Version: Trunk +License: Apache License 2.0 + +Description: Google's core Java libraries. + +Local Modifications: None. + + +---- +Code in: +lib/junit.jar + +URL: http://sourceforge.net/projects/junit/ +Version: 3.8.1 +License: Common Public License 1.0 + +Description: A framework for writing and running automated tests in Java. + +Local Modifications: None. + + diff --git a/lib/compiler-closure/compiler.jar b/lib/compiler-closure/compiler.jar new file mode 100644 index 00000000..da053a7d Binary files /dev/null and b/lib/compiler-closure/compiler.jar differ diff --git a/lib/jasmine-jstd-adapter/JasmineAdapter.js b/lib/jasmine-jstd-adapter/JasmineAdapter.js new file mode 100644 index 00000000..0fdc4612 --- /dev/null +++ b/lib/jasmine-jstd-adapter/JasmineAdapter.js @@ -0,0 +1,111 @@ +/** + * @fileoverview Jasmine JsTestDriver Adapter. + * @author ibolmo@gmail.com (Olmo Maldonado) + * @author misko@hevery.com (Misko Hevery) + */ + +(function() { + + function bind(_this, _function){ + return function(){ + return _function.call(_this); + }; + } + + var currentFrame = frame(null, null); + + function frame(parent, name){ + var caseName = (parent && parent.caseName ? parent.caseName + " " : '') + (name ? name : ''); + var frame = { + name: name, + caseName: caseName, + parent: parent, + testCase: TestCase(caseName), + before: [], + after: [], + runBefore: function(){ + if (parent) parent.runBefore.apply(this); + for ( var i = 0; i < frame.before.length; i++) { + frame.before[i].apply(this); + } + }, + runAfter: function(){ + for ( var i = 0; i < frame.after.length; i++) { + frame.after[i].apply(this); + } + if (parent) parent.runAfter.apply(this); + } + }; + return frame; + }; + + jasmine.Env.prototype.describe = (function(describe){ + return function(description){ + currentFrame = frame(currentFrame, description); + var val = describe.apply(this, arguments); + currentFrame = currentFrame.parent; + return val; + }; + + })(jasmine.Env.prototype.describe); + + var id = 0; + + jasmine.Env.prototype.it = (function(it){ + return function(desc, itFn){ + var self = this; + var spec = it.apply(this, arguments); + var currentSpec = this.currentSpec; + if (!currentSpec.$id) { + currentSpec.$id = id++; + } + var frame = this.jstdFrame = currentFrame; + var name = 'test that it ' + desc; + if (this.jstdFrame.testCase.prototype[name]) + throw "Spec with name '" + desc + "' already exists."; + this.jstdFrame.testCase.prototype[name] = function(){ + jasmine.getEnv().currentSpec = currentSpec; + frame.runBefore.apply(currentSpec); + try { + itFn.apply(currentSpec); + } finally { + frame.runAfter.apply(currentSpec); + } + }; + return spec; + }; + + })(jasmine.Env.prototype.it); + + + jasmine.Env.prototype.beforeEach = (function(beforeEach){ + return function(beforeEachFunction) { + beforeEach.apply(this, arguments); + currentFrame.before.push(beforeEachFunction); + }; + + })(jasmine.Env.prototype.beforeEach); + + + jasmine.Env.prototype.afterEach = (function(afterEach){ + return function(afterEachFunction) { + afterEach.apply(this, arguments); + currentFrame.after.push(afterEachFunction); + }; + + })(jasmine.Env.prototype.afterEach); + + + jasmine.NestedResults.prototype.addResult = (function(addResult){ + return function(result) { + addResult.call(this, result); + if (result.type != 'MessageResult' && !result.passed()) fail(result.message); + }; + + })(jasmine.NestedResults.prototype.addResult); + + // Reset environment with overriden methods. + jasmine.currentEnv_ = null; + jasmine.getEnv(); + +})(); diff --git a/lib/jasmine/jasmine-0.10.3.js b/lib/jasmine/jasmine-0.10.3.js new file mode 100644 index 00000000..f309493f --- /dev/null +++ b/lib/jasmine/jasmine-0.10.3.js @@ -0,0 +1,2331 @@ +/** + * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. + * + * @namespace + */ +var jasmine = {}; + +/** + * @private + */ +jasmine.unimplementedMethod_ = function() { + throw new Error("unimplemented method"); +}; + +/** + * Use jasmine.undefined instead of undefined, since undefined is just + * a plain old variable and may be redefined by somebody else. + * + * @private + */ +jasmine.undefined = jasmine.___undefined___; + +/** + * Default interval for event loop yields. Small values here may result in slow test running. Zero means no updates until all tests have completed. + * + */ +jasmine.DEFAULT_UPDATE_INTERVAL = 250; + +/** + * Allows for bound functions to be compared. Internal use only. + * + * @ignore + * @private + * @param base {Object} bound 'this' for the function + * @param name {Function} function to find + */ +jasmine.bindOriginal_ = function(base, name) { + var original = base[name]; + if (original.apply) { + return function() { + return original.apply(base, arguments); + }; + } else { + // IE support + return window[name]; + } +}; + +jasmine.setTimeout = jasmine.bindOriginal_(window, 'setTimeout'); +jasmine.clearTimeout = jasmine.bindOriginal_(window, 'clearTimeout'); +jasmine.setInterval = jasmine.bindOriginal_(window, 'setInterval'); +jasmine.clearInterval = jasmine.bindOriginal_(window, 'clearInterval'); + +jasmine.MessageResult = function(text) { + this.type = 'MessageResult'; + this.text = text; + this.trace = new Error(); // todo: test better +}; + +jasmine.ExpectationResult = function(params) { + this.type = 'ExpectationResult'; + this.matcherName = params.matcherName; + this.passed_ = params.passed; + this.expected = params.expected; + this.actual = params.actual; + + /** @deprecated */ + this.details = params.details; + + this.message = this.passed_ ? 'Passed.' : params.message; + this.trace = this.passed_ ? '' : new Error(this.message); +}; + +jasmine.ExpectationResult.prototype.passed = function () { + return this.passed_; +}; + +/** + * Getter for the Jasmine environment. Ensures one gets created + */ +jasmine.getEnv = function() { + return jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isArray_ = function(value) { + return jasmine.isA_("Array", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isString_ = function(value) { + return jasmine.isA_("String", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isNumber_ = function(value) { + return jasmine.isA_("Number", value); +}; + +/** + * @ignore + * @private + * @param {String} typeName + * @param value + * @returns {Boolean} + */ +jasmine.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; +}; + +/** + * Pretty printer for expecations. Takes any object and turns it into a human-readable string. + * + * @param value {Object} an object to be outputted + * @returns {String} + */ +jasmine.pp = function(value) { + var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; +}; + +/** + * Returns true if the object is a DOM Node. + * + * @param {Object} obj object to check + * @returns {Boolean} + */ +jasmine.isDomNode = function(obj) { + return obj['nodeType'] > 0; +}; + +/** + * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. + * + * @example + * // don't care about which function is passed in, as long as it's a function + * expect(mySpy).wasCalledWith(jasmine.any(Function)); + * + * @param {Class} clazz + * @returns matchable object of the type clazz + */ +jasmine.any = function(clazz) { + return new jasmine.Matchers.Any(clazz); +}; + +/** + * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. + * + * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine + * expectation syntax. Spies can be checked if they were called or not and what the calling params were. + * + * A Spy has the following mehtod: wasCalled, callCount, mostRecentCall, and argsForCall (see docs) + * Spies are torn down at the end of every spec. + * + * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. + * + * @example + * // a stub + * var myStub = jasmine.createSpy('myStub'); // can be used anywhere + * + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // actual foo.not will not be called, execution stops + * spyOn(foo, 'not'); + + // foo.not spied upon, execution will continue to implementation + * spyOn(foo, 'not').andCallThrough(); + * + * // fake example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // foo.not(val) will return val + * spyOn(foo, 'not').andCallFake(function(value) {return value;}); + * + * // mock example + * foo.not(7 == 7); + * expect(foo.not).wasCalled(); + * expect(foo.not).wasCalledWith(true); + * + * @constructor + * @see spyOn, jasmine.createSpy, jasmine.createSpyObj + * @param {String} name + */ +jasmine.Spy = function(name) { + /** + * The name of the spy, if provided. + */ + this.identity = name || 'unknown'; + /** + * Is this Object a spy? + */ + this.isSpy = true; + /** + * The actual function this spy stubs. + */ + this.plan = function() { + }; + /** + * Tracking of the most recent call to the spy. + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy.mostRecentCall.args = [1, 2]; + */ + this.mostRecentCall = {}; + + /** + * Holds arguments for each call to the spy, indexed by call count + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy(7, 8); + * mySpy.mostRecentCall.args = [7, 8]; + * mySpy.argsForCall[0] = [1, 2]; + * mySpy.argsForCall[1] = [7, 8]; + */ + this.argsForCall = []; + this.calls = []; +}; + +/** + * Tells a spy to call through to the actual implemenatation. + * + * @example + * var foo = { + * bar: function() { // do some stuff } + * } + * + * // defining a spy on an existing property: foo.bar + * spyOn(foo, 'bar').andCallThrough(); + */ +jasmine.Spy.prototype.andCallThrough = function() { + this.plan = this.originalValue; + return this; +}; + +/** + * For setting the return value of a spy. + * + * @example + * // defining a spy from scratch: foo() returns 'baz' + * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); + * + * // defining a spy on an existing property: foo.bar() returns 'baz' + * spyOn(foo, 'bar').andReturn('baz'); + * + * @param {Object} value + */ +jasmine.Spy.prototype.andReturn = function(value) { + this.plan = function() { + return value; + }; + return this; +}; + +/** + * For throwing an exception when a spy is called. + * + * @example + * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' + * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); + * + * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' + * spyOn(foo, 'bar').andThrow('baz'); + * + * @param {String} exceptionMsg + */ +jasmine.Spy.prototype.andThrow = function(exceptionMsg) { + this.plan = function() { + throw exceptionMsg; + }; + return this; +}; + +/** + * Calls an alternate implementation when a spy is called. + * + * @example + * var baz = function() { + * // do some stuff, return something + * } + * // defining a spy from scratch: foo() calls the function baz + * var foo = jasmine.createSpy('spy on foo').andCall(baz); + * + * // defining a spy on an existing property: foo.bar() calls an anonymnous function + * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); + * + * @param {Function} fakeFunc + */ +jasmine.Spy.prototype.andCallFake = function(fakeFunc) { + this.plan = fakeFunc; + return this; +}; + +/** + * Resets all of a spy's the tracking variables so that it can be used again. + * + * @example + * spyOn(foo, 'bar'); + * + * foo.bar(); + * + * expect(foo.bar.callCount).toEqual(1); + * + * foo.bar.reset(); + * + * expect(foo.bar.callCount).toEqual(0); + */ +jasmine.Spy.prototype.reset = function() { + this.wasCalled = false; + this.callCount = 0; + this.argsForCall = []; + this.calls = []; + this.mostRecentCall = {}; +}; + +jasmine.createSpy = function(name) { + + var spyObj = function() { + spyObj.wasCalled = true; + spyObj.callCount++; + var args = jasmine.util.argsToArray(arguments); + spyObj.mostRecentCall.object = this; + spyObj.mostRecentCall.args = args; + spyObj.argsForCall.push(args); + spyObj.calls.push({object: this, args: args}); + return spyObj.plan.apply(this, arguments); + }; + + var spy = new jasmine.Spy(name); + + for (var prop in spy) { + spyObj[prop] = spy[prop]; + } + + spyObj.reset(); + + return spyObj; +}; + +/** + * Determines whether an object is a spy. + * + * @param {jasmine.Spy|Object} putativeSpy + * @returns {Boolean} + */ +jasmine.isSpy = function(putativeSpy) { + return putativeSpy && putativeSpy.isSpy; +}; + +/** + * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something + * large in one call. + * + * @param {String} baseName name of spy class + * @param {Array} methodNames array of names of methods to make spies + */ +jasmine.createSpyObj = function(baseName, methodNames) { + if (!jasmine.isArray_(methodNames) || methodNames.length == 0) { + throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); + } + return obj; +}; + +jasmine.log = function(message) { + jasmine.getEnv().currentSpec.log(message); +}; + +/** + * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. + * + * @example + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops + * + * @see jasmine.createSpy + * @param obj + * @param methodName + * @returns a Jasmine spy that can be chained with all spy methods + */ +var spyOn = function(obj, methodName) { + return jasmine.getEnv().currentSpec.spyOn(obj, methodName); +}; + +/** + * Creates a Jasmine spec that will be added to the current suite. + * + * // TODO: pending tests + * + * @example + * it('should be true', function() { + * expect(true).toEqual(true); + * }); + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var it = function(desc, func) { + return jasmine.getEnv().it(desc, func); +}; + +/** + * Creates a disabled Jasmine spec. + * + * A convenience method that allows existing specs to be disabled temporarily during development. + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var xit = function(desc, func) { + return jasmine.getEnv().xit(desc, func); +}; + +/** + * Starts a chain for a Jasmine expectation. + * + * It is passed an Object that is the actual value and should chain to one of the many + * jasmine.Matchers functions. + * + * @param {Object} actual Actual value to test against and expected value + */ +var expect = function(actual) { + return jasmine.getEnv().currentSpec.expect(actual); +}; + +/** + * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. + * + * @param {Function} func Function that defines part of a jasmine spec. + */ +var runs = function(func) { + jasmine.getEnv().currentSpec.runs(func); +}; + +/** + * Waits for a timeout before moving to the next runs()-defined block. + * @param {Number} timeout + */ +var waits = function(timeout) { + jasmine.getEnv().currentSpec.waits(timeout); +}; + +/** + * Waits for the latchFunction to return true before proceeding to the next runs()-defined block. + * + * @param {Number} timeout + * @param {Function} latchFunction + * @param {String} message + */ +var waitsFor = function(timeout, latchFunction, message) { + jasmine.getEnv().currentSpec.waitsFor(timeout, latchFunction, message); +}; + +/** + * A function that is called before each spec in a suite. + * + * Used for spec setup, including validating assumptions. + * + * @param {Function} beforeEachFunction + */ +var beforeEach = function(beforeEachFunction) { + jasmine.getEnv().beforeEach(beforeEachFunction); +}; + +/** + * A function that is called after each spec in a suite. + * + * Used for restoring any state that is hijacked during spec execution. + * + * @param {Function} afterEachFunction + */ +var afterEach = function(afterEachFunction) { + jasmine.getEnv().afterEach(afterEachFunction); +}; + +/** + * Defines a suite of specifications. + * + * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared + * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization + * of setup in some tests. + * + * @example + * // TODO: a simple suite + * + * // TODO: a simple suite with a nested describe block + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var describe = function(description, specDefinitions) { + return jasmine.getEnv().describe(description, specDefinitions); +}; + +/** + * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var xdescribe = function(description, specDefinitions) { + return jasmine.getEnv().xdescribe(description, specDefinitions); +}; + + +// Provide the XMLHttpRequest class for IE 5.x-6.x: +jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { + try { + return new ActiveXObject("Msxml2.XMLHTTP.6.0"); + } catch(e) { + } + try { + return new ActiveXObject("Msxml2.XMLHTTP.3.0"); + } catch(e) { + } + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch(e) { + } + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch(e) { + } + throw new Error("This browser does not support XMLHttpRequest."); +} : XMLHttpRequest; + +/** + * Adds suite files to an HTML document so that they are executed, thus adding them to the current + * Jasmine environment. + * + * @param {String} url path to the file to include + * @param {Boolean} opt_global + * @deprecated We suggest you use a different method of including JS source files. jasmine.include will be removed soon. + */ +jasmine.include = function(url, opt_global) { + if (opt_global) { + document.write(' + + + + + diff --git a/scenario/Runner.html b/scenario/Runner.html new file mode 100644 index 00000000..ffa08af9 --- /dev/null +++ b/scenario/Runner.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/scenario/application-account.html b/scenario/application-account.html new file mode 100644 index 00000000..a43deffc --- /dev/null +++ b/scenario/application-account.html @@ -0,0 +1,6 @@ +
+account page goes here! + + +
+ diff --git a/scenario/application.html b/scenario/application.html new file mode 100644 index 00000000..6b6ced69 --- /dev/null +++ b/scenario/application.html @@ -0,0 +1,34 @@ + + + + + + + + + [ login + | account + ] + + +
login screen
+ +
+ + + (( input name )) + +
$location={{$location}}
+ + diff --git a/scenario/cross-site-post/People.json b/scenario/cross-site-post/People.json new file mode 100644 index 00000000..de51fd83 --- /dev/null +++ b/scenario/cross-site-post/People.json @@ -0,0 +1,4 @@ +[ + { name: 'Misko', favorite: ['water melon', 'persimmon', 'passion fruit'] }, + { name: 'Lenka', favorite: ['strawberry'] } +] diff --git a/scenario/cross-site-post/index.html b/scenario/cross-site-post/index.html new file mode 100644 index 00000000..3ff6af85 --- /dev/null +++ b/scenario/cross-site-post/index.html @@ -0,0 +1,10 @@ + + + + + + + +
people = {{people}}
+ + diff --git a/scenario/datastore-scenarios.js b/scenario/datastore-scenarios.js new file mode 100644 index 00000000..6038070b --- /dev/null +++ b/scenario/datastore-scenarios.js @@ -0,0 +1,19 @@ +angular.scenarioDef.datastore = { + $before:[ + {Given:"dataset", + dataset:{ + Book:[{$id:'moby', name:"Moby Dick"}, + {$id:'gadsby', name:'Great Gadsby'}] + } + }, + {Given:"browser", at:"datastore.html#book=moby"}, + ], + checkLoadBook:[ + {Then:"drainRequestQueue"}, + + {Then:"text", at:"{{book.$id}}", should_be:"moby"}, + {Then:"text", at:"li[$index=0] {{book.name}}", should_be:"Great Gahdsby"}, + {Then:"text", at:"li[$index=0] {{book.name}}", should_be:"Moby Dick"}, + + ] +}; diff --git a/scenario/datastore.html b/scenario/datastore.html new file mode 100644 index 00000000..525d3636 --- /dev/null +++ b/scenario/datastore.html @@ -0,0 +1,17 @@ + + + + + + + + + +

{{book.$id}}

+
  • +
  • {{book.name}}
  • + + + diff --git a/scenario/perf.html b/scenario/perf.html new file mode 100644 index 00000000..cd676918 --- /dev/null +++ b/scenario/perf.html @@ -0,0 +1,33 @@ + + + + + + + + + +
    + + + diff --git a/scenario/style.css b/scenario/style.css new file mode 100644 index 00000000..956bdc52 --- /dev/null +++ b/scenario/style.css @@ -0,0 +1,7 @@ +th { + text-align: left; +} + +tr { + border: 1px solid black; +} diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js new file mode 100644 index 00000000..f4488190 --- /dev/null +++ b/scenario/widgets-scenario.js @@ -0,0 +1,25 @@ +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(''); + input('text.password').enter('secret'); + expect('{{text.password}}').toEqual('secret'); + + expect('{{text.hidden}}').toEqual('hiddenValue'); + + expect('{{gender}}').toEqual('male'); + input('gender').select('female'); + input('gender').isChecked('female'); + expect('{{gender}}').toEqual('female'); + +// expect('{{tea}}').toBeChecked(); +// input('gender').select('female'); +// expect('{{gender}}').toEqual('female'); + + }); +}); diff --git a/scenario/widgets-scenarios.old b/scenario/widgets-scenarios.old new file mode 100644 index 00000000..a1e6c0ed --- /dev/null +++ b/scenario/widgets-scenarios.old @@ -0,0 +1,49 @@ +angular.scenarioDef.widgets = { + $before:[ + {Given:"browser", at:"widgets.html"} + ], + checkWidgetBinding:[ + {Then:"text", at:"{{text.basic}}", should_be:""}, + {When:"enter", text:"John", at:":input[name=text.basic]"}, + {Then:"text", at:"{{text.basic}}", should_be:"John"}, + + {Then:"text", at:"{{gender}}", should_be:"male"}, + {When:"click", at:"input:radio[value=female]"}, + {Then:"text", at:"{{gender}}", should_be:"female"}, + + {Then:"text", at:"{{tea}}", should_be:"on"}, + {When:"click", at:"input[name=tea]"}, + {Then:"text", at:"{{tea}}", should_be:""}, + + {Then:"text", at:"{{coffee}}", should_be:""}, + {When:"click", at:"input[name=coffee]"}, + {Then:"text", at:"{{coffee}}", should_be:"on"}, + + {Then:"text", at:"{{count}}", should_be:0}, + {When:"click", at:"form :button"}, + {When:"click", at:"form :submit"}, + {When:"click", at:"form :image"}, + {Then:"text", at:"{{count}}", should_be:3}, + + {Then:"text", at:"{{select}}", should_be:"A"}, + {When:"select", at:"select[name=select]", option:"B"}, + {Then:"text", at:"{{select}}", should_be:"B"}, + + {Then:"text", at:"{{multiple}}", should_be:"[]"}, + {When:"select", at:"select[name=multiple]", option:"A"}, + {Then:"text", at:"{{multiple}}", should_be:["A"]}, + {When:"select", at:"select[name=multiple]", option:"B"}, + {Then:"text", at:"{{multiple}}", should_be:["A", "B"]}, + {When:"select", at:"select[name=multiple]", option:"A"}, + {Then:"text", at:"{{multiple}}", should_be:["B"]}, + + {Then:"text", at:"{{hidden}}", should_be:"hiddenValue"}, + + {Then:"text", at:"{{password}}", should_be:"passwordValue"}, + {When:"enter", text:"reset", at:":input[name=password]"}, + {Then:"text", at:"{{password}}", should_be:"reset"}, + ], + checkNewWidgetEmpty:[ + {Then:"text", at:"{{name}}", should_be:""}, + ] +}; diff --git a/scenario/widgets.html b/scenario/widgets.html new file mode 100644 index 00000000..86269e86 --- /dev/null +++ b/scenario/widgets.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DescriptionTestResult
    Input text field
    basic + + text.basic={{text.basic}}
    passwordtext.password={{text.password}}
    hiddentext.hidden={{text.hidden}}
    Input selection field
    radio + Female
    + Male +
    gender={{gender}}
    checkbox + Tea
    + Coffe +
    +
    checkbox={{checkbox}}
    +
    select + + select={{select}}
    multiselect + + multiselect={{multiselect}}
    Buttons
    ng-change
    ng-click
    +
    +
    +
    +
    + action +
    +
    button={{button}}
    Repeaters
    ng-repeat +
      +
    • {{name}}
    • +
    +
    + + diff --git a/server.sh b/server.sh new file mode 100755 index 00000000..7690cf8a --- /dev/null +++ b/server.sh @@ -0,0 +1 @@ +java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 diff --git a/src/Angular.js b/src/Angular.js new file mode 100644 index 00000000..2b26c88d --- /dev/null +++ b/src/Angular.js @@ -0,0 +1,401 @@ +//////////////////////////////////// + +if (typeof document.getAttribute == 'undefined') + document.getAttribute = function() {}; + +if (!window['console']) window['console']={'log':noop, 'error':noop}; + +var consoleNode, + PRIORITY_FIRST = -99999, + PRIORITY_WATCH = -1000, + PRIORITY_LAST = 99999, + PRIORITY = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH}, + NOOP = 'noop', + NG_EXCEPTION = 'ng-exception', + NG_VALIDATION_ERROR = 'ng-validation-error', + jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy + _ = window['_'], + msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)), + jqLite = jQuery || jqLiteWrap, + slice = Array.prototype.slice, + angular = window['angular'] || (window['angular'] = {}), + angularTextMarkup = extensionMap(angular, 'textMarkup'), + angularAttrMarkup = extensionMap(angular, 'attrMarkup'), + angularDirective = extensionMap(angular, 'directive'), + angularWidget = extensionMap(angular, 'widget'), + angularValidator = extensionMap(angular, 'validator'), + angularFilter = extensionMap(angular, 'filter'), + angularFormatter = extensionMap(angular, 'formatter'), + angularService = extensionMap(angular, 'service'), + angularCallbacks = extensionMap(angular, 'callbacks'), + nodeName; + +function angularAlert(){ + log(arguments); window.alert.apply(window, arguments); +} + +function foreach(obj, iterator, context) { + var key; + if (obj) { + if (isFunction(obj)){ + for (key in obj) { + if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key); + } + } + } else if (obj.forEach) { + obj.forEach(iterator, context); + } else if (isObject(obj) && isNumber(obj.length)) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) + iterator.call(context, obj[key], key); + } + } + return obj; +} + +function foreachSorted(obj, iterator, context) { + var keys = []; + for (var key in obj) keys.push(key); + keys.sort(); + for ( var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + +function extend(dst) { + foreach(arguments, function(obj){ + if (obj !== dst) { + foreach(obj, function(value, key){ + dst[key] = value; + }); + } + }); + return dst; +} + +function noop() {} +function identity($) {return $;} +function extensionMap(angular, name) { + var extPoint; + return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + if (isDefined(fn)) { + extPoint[name] = extend(fn, prop || {}); + } + return extPoint[name]; + }); +} + +function jqLiteWrap(element) { + // for some reasons the parentNode of an orphan looks like null but its typeof is object. + if (element) { + if (isString(element)) { + var div = document.createElement('div'); + div.innerHTML = element; + element = new JQLite(div.childNodes); + } else if (!(element instanceof JQLite) && isElement(element)) { + element = new JQLite(element); + } + } + return element; +} +function isUndefined(value){ return typeof value == 'undefined'; } +function isDefined(value){ return typeof value != 'undefined'; } +function isObject(value){ return typeof value == 'object';} +function isString(value){ return typeof value == 'string';} +function isNumber(value){ return typeof value == 'number';} +function isArray(value) { return value instanceof Array; } +function isFunction(value){ return typeof value == 'function';} +function isTextNode(node) { return nodeName(node) == '#text'; } +function lowercase(value){ return isString(value) ? value.toLowerCase() : value; } +function uppercase(value){ return isString(value) ? value.toUpperCase() : value; } +function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; } +function isElement(node) { + return node && (node.nodeName || node instanceof JQLite || (jQuery && node instanceof jQuery)); +} + +function HTML(html) { + this.html = html; +} + +if (msie) { + nodeName = function(element) { + element = element[0] || element; + return (element.scopeName && element.scopeName != 'HTML' ) ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; + }; +} else { + nodeName = function(element) { + return (element[0] || element).nodeName; + }; +} + +function isVisible(element) { + var rect = element[0].getBoundingClientRect(), + width = (rect.width || (rect.right||0 - rect.left||0)), + height = (rect.height || (rect.bottom||0 - rect.top||0)); + return width>0 && height>0; +} + +function map(obj, iterator, context) { + var results = []; + foreach(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; +} +function size(obj) { + var size = 0; + if (obj) { + if (isNumber(obj.length)) { + return obj.length; + } else if (isObject(obj)){ + for (key in obj) + size++; + } + } + return size; +} +function includes(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return true; + } + return false; +} + +function indexOf(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return i; + } + return -1; +} + +function log(a, b, c){ + var console = window['console']; + switch(arguments.length) { + case 1: + console['log'](a); + break; + case 2: + console['log'](a, b); + break; + default: + console['log'](a, b, c); + break; + } +} + +function error(a, b, c){ + var console = window['console']; + switch(arguments.length) { + case 1: + console['error'](a); + break; + case 2: + console['error'](a, b); + break; + default: + console['error'](a, b, c); + break; + } +} + +function consoleLog(level, objs) { + var log = document.createElement("div"); + log.className = level; + var msg = ""; + var sep = ""; + for ( var i = 0; i < objs.length; i++) { + var obj = objs[i]; + msg += sep + (typeof obj == 'string' ? obj : toJson(obj)); + sep = " "; + } + log.appendChild(document.createTextNode(msg)); + consoleNode.appendChild(log); +} + +function isLeafNode (node) { + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } + } + return false; +} + +function copy(source, destination){ + if (!destination) { + if (source) { + if (isArray(source)) { + return copy(source, []); + } else if (isObject(source)) { + return copy(source, {}); + } + } + return source; + } else { + if (isArray(source)) { + while(destination.length) { + destination.pop(); + } + for ( var i = 0; i < source.length; i++) { + destination.push(copy(source[i])); + } + } else { + foreach(destination, function(value, key){ + delete destination[key]; + }); + for ( var key in source) { + destination[key] = copy(source[key]); + } + } + return destination; + } +} + +function setHtml(node, html) { + if (isLeafNode(node)) { + if (msie) { + node.innerText = html; + } else { + node.textContent = html; + } + } else { + node.innerHTML = html; + } +} + +function escapeHtml(html) { + if (!html || !html.replace) + return html; + return html. + replace(/&/g, '&'). + replace(//g, '>'); +} + + +function isRenderableElement(element) { + var name = element && element[0] && element[0].nodeName; + return name && name.charAt(0) != '#' && + !includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name); +} +function elementError(element, type, error) { + while (!isRenderableElement(element)) { + element = element.parent() || jqLite(document.body); + } + if (element[0]['$NG_ERROR'] !== error) { + element[0]['$NG_ERROR'] = error; + if (error) { + element.addClass(type); + element.attr(type, error); + } else { + element.removeClass(type); + element.removeAttr(type); + } + } +} + +function escapeAttr(html) { + if (!html || !html.replace) + return html; + return html.replace(//g, '>').replace(/\"/g, + '"'); +} + +function bind(_this, _function) { + if (!isFunction(_function)) + throw "Not a function!"; + var curryArgs = slice.call(arguments, 2, arguments.length); + return function() { + return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); + }; +} + +function outerHTML(node) { + var temp = document.createElement('div'); + temp.appendChild(node); + var outerHTML = temp.innerHTML; + temp.removeChild(node); + return outerHTML; +} + +function toBoolean(value) { + if (value && value.length !== 0) { + var v = lowercase("" + value); + value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == '[]'); + } else { + value = false; + } + return value; +} + +function merge(src, dst) { + for ( var key in src) { + var value = dst[key]; + var type = typeof value; + if (type == 'undefined') { + dst[key] = fromJson(toJson(src[key])); + } else if (type == 'object' && value.constructor != array && + key.substring(0, 1) != "$") { + merge(src[key], value); + } + } +} + +function compile(element, parentScope, overrides) { + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget), + $element = jqLite(element), + parent = extend({}, parentScope); + parent.$element = $element; + return compiler.compile($element)($element, parent, overrides); +} +///////////////////////////////////////////////// + +function parseKeyValue(keyValue) { + var obj = {}, key_value, key; + foreach((keyValue || "").split('&'), function(keyValue){ + if (keyValue) { + key_value = keyValue.split('='); + key = decodeURIComponent(key_value[0]); + obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true; + } + }); + return obj; +} + +function toKeyValue(obj) { + var parts = []; + foreach(obj, function(value, key){ + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return parts.length ? parts.join('&') : ''; +} + +function angularInit(config){ + if (config.autobind) { + var scope = compile(window.document, null, {'$config':config}); + // TODO default to the source of angular.js + scope.$browser.addCss('css/angular.css'); + scope.$init(); + } +} + +function angularJsConfig(document) { + var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/, + scripts = document.getElementsByTagName("SCRIPT"), + match; + for(var j = 0; j < scripts.length; j++) { + match = (scripts[j].src || "").match(filename); + if (match) { + return match[5]; + } + } + return ""; +} diff --git a/src/AngularPublic.js b/src/AngularPublic.js new file mode 100644 index 00000000..7230c3e5 --- /dev/null +++ b/src/AngularPublic.js @@ -0,0 +1,29 @@ +var browserSingleton; +angularService('$browser', function browserFactory(){ + if (!browserSingleton) { + browserSingleton = new Browser(window.location, window.document); + browserSingleton.startUrlWatcher(); + browserSingleton.bind(); + } + return browserSingleton; +}); + +extend(angular, { + 'element': jqLite, + 'compile': compile, + 'scope': createScope, + 'copy': copy, + 'extend': extend, + 'foreach': foreach, + 'noop':noop, + 'bind':bind, + 'identity':identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isObject': isObject, + 'isNumber': isNumber, + 'isArray': isArray +}); + diff --git a/src/Browser.js b/src/Browser.js new file mode 100644 index 00000000..0552b3ae --- /dev/null +++ b/src/Browser.js @@ -0,0 +1,130 @@ +////////////////////////////// +// Browser +////////////////////////////// + +function Browser(location, document) { + this.delay = 50; + this.expectedUrl = location.href; + this.urlListeners = []; + this.hoverListener = noop; + this.isMock = false; + this.outstandingRequests = { count: 0, callbacks:[]}; + + this.XHR = window.XMLHttpRequest || function () { + try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} + try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} + throw new Error("This browser does not support XMLHttpRequest."); + }; + this.setTimeout = function(fn, delay) { + window.setTimeout(fn, delay); + }; + + this.location = location; + this.document = jqLite(document); + this.body = jqLite(document.body); +} + +Browser.prototype = { + + bind: function() { + var self = this; + self.document.bind("mouseover", function(event){ + self.hoverListener(jqLite(msie ? event.srcElement : event.target), true); + return true; + }); + self.document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){ + self.hoverListener(jqLite(event.target), false); + return true; + }); + }, + + hover: function(hoverListener) { + this.hoverListener = hoverListener; + }, + + addCss: function(url) { + var doc = this.document[0], + head = jqLite(doc.getElementsByTagName('head')[0]), + link = jqLite(doc.createElement('link')); + link.attr('rel', 'stylesheet'); + link.attr('type', 'text/css'); + link.attr('href', url); + head.append(link); + }, + + xhr: function(method, url, post, callback){ + if (isFunction(post)) { + callback = post; + post = null; + } + var xhr = new this.XHR(), + self = this; + xhr.open(method, url, true); + this.outstandingRequests.count ++; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + try { + callback(xhr.status || 200, xhr.responseText); + } finally { + self.outstandingRequests.count--; + self.processRequestCallbacks(); + } + } + }; + xhr.send(post || ''); + }, + + processRequestCallbacks: function(){ + if (this.outstandingRequests.count === 0) { + while(this.outstandingRequests.callbacks.length) { + try { + this.outstandingRequests.callbacks.pop()(); + } catch (e) { + } + } + } + }, + + notifyWhenNoOutstandingRequests: function(callback){ + if (this.outstandingRequests.count === 0) { + callback(); + } else { + this.outstandingRequests.callbacks.push(callback); + } + }, + + watchUrl: function(fn){ + this.urlListeners.push(fn); + }, + + startUrlWatcher: function() { + var self = this; + (function pull () { + if (self.expectedUrl !== self.location.href) { + foreach(self.urlListeners, function(listener){ + try { + listener(self.location.href); + } catch (e) { + error(e); + } + }); + self.expectedUrl = self.location.href; + } + self.setTimeout(pull, self.delay); + })(); + }, + + setUrl: function(url) { + var existingURL = this.location.href; + if (!existingURL.match(/#/)) existingURL += '#'; + if (!url.match(/#/)) url += '#'; + if (existingURL != url) { + this.location.href = this.expectedUrl = url; + } + }, + + getUrl: function() { + return this.location.href; + } +}; diff --git a/src/Compiler.js b/src/Compiler.js new file mode 100644 index 00000000..c8910c27 --- /dev/null +++ b/src/Compiler.js @@ -0,0 +1,212 @@ +/** + * Template provides directions an how to bind to a given element. + * It contains a list of init functions which need to be called to + * bind to a new instance of elements. It also provides a list + * of child paths which contain child templates + */ +function Template(priority) { + this.paths = []; + this.children = []; + this.inits = []; + this.priority = priority || 0; +} + +Template.prototype = { + init: function(element, scope) { + var inits = {}; + this.collectInits(element, inits); + foreachSorted(inits, function(queue){ + foreach(queue, function(fn){ + fn(scope); + }); + }); + }, + + collectInits: function(element, inits) { + var queue = inits[this.priority]; + if (!queue) { + inits[this.priority] = queue = []; + } + element = jqLite(element); + foreach(this.inits, function(fn) { + queue.push(function(scope) { + scope.$tryEval(fn, element, element); + }); + }); + + var i, + childNodes = element[0].childNodes, + children = this.children, + paths = this.paths, + length = paths.length; + for (i = 0; i < length; i++) { + children[i].collectInits(childNodes[paths[i]], inits); + } + }, + + + addInit:function(init) { + if (init) { + this.inits.push(init); + } + }, + + + addChild: function(index, template) { + if (template) { + this.paths.push(index); + this.children.push(template); + } + }, + + empty: function() { + return this.inits.length === 0 && this.paths.length === 0; + } +}; + +/////////////////////////////////// +//Compiler +////////////////////////////////// +function Compiler(textMarkup, attrMarkup, directives, widgets){ + this.textMarkup = textMarkup; + this.attrMarkup = attrMarkup; + this.directives = directives; + this.widgets = widgets; +} + +Compiler.prototype = { + compile: function(rawElement) { + rawElement = jqLite(rawElement); + var index = 0, + template, + parent = rawElement.parent(); + if (parent && parent[0]) { + parent = parent[0]; + for(var i = 0; i < parent.childNodes.length; i++) { + if (parent.childNodes[i] == rawElement[0]) { + index = i; + } + } + } + template = this.templatize(rawElement, index, 0) || new Template(); + return function(element, parentScope){ + element = jqLite(element); + var scope = parentScope && parentScope.$eval ? + parentScope : + createScope(parentScope || {}, angularService); + return extend(scope, { + $element:element, + $init: function() { + template.init(element, scope); + scope.$eval(); + delete scope.$init; + return scope; + } + }); + }; + }, + + templatize: function(element, elementIndex, priority){ + var self = this, + widget, + directiveFns = self.directives, + descend = true, + directives = true, + template, + selfApi = { + compile: bind(self, self.compile), + comment:function(text) {return jqLite(document.createComment(text));}, + element:function(type) {return jqLite(document.createElement(type));}, + text:function(text) {return jqLite(document.createTextNode(text));}, + descend: function(value){ if(isDefined(value)) descend = value; return descend;}, + directives: function(value){ if(isDefined(value)) directives = value; return directives;} + }; + priority = element.attr('ng-eval-order') || priority || 0; + if (isString(priority)) { + priority = PRIORITY[uppercase(priority)] || 0; + } + template = new Template(priority); + eachAttribute(element, function(value, name){ + if (!widget) { + if (widget = self.widgets['@' + name]) { + widget = bind(selfApi, widget, value, element); + } + } + }); + if (!widget) { + if (widget = self.widgets[nodeName(element)]) { + widget = bind(selfApi, widget, element); + } + } + if (widget) { + descend = false; + directives = false; + var parent = element.parent(); + template.addInit(widget.call(selfApi, element)); + if (parent && parent[0]) { + element = jqLite(parent[0].childNodes[elementIndex]); + } + } + if (descend){ + // process markup for text nodes only + eachTextNode(element, function(textNode){ + var text = textNode.text(); + foreach(self.textMarkup, function(markup){ + markup.call(selfApi, text, textNode, element); + }); + }); + } + + if (directives) { + // Process attributes/directives + eachAttribute(element, function(value, name){ + foreach(self.attrMarkup, function(markup){ + markup.call(selfApi, value, name, element); + }); + }); + eachAttribute(element, function(value, name){ + template.addInit((directiveFns[name]||noop).call(selfApi, value, element)); + }); + } + // Process non text child nodes + if (descend) { + eachNode(element, function(child, i){ + template.addChild(i, self.templatize(child, i, priority)); + }); + } + return template.empty() ? null : template; + } +}; + +function eachTextNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], chld; + for (i = 0; i < chldNodes.length; i++) { + if(isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); + } + } +} + +function eachNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], chld; + for (i = 0; i < chldNodes.length; i++) { + if(!isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); + } + } +} + +function eachAttribute(element, fn){ + var i, attrs = element[0].attributes || [], chld, attr, name, value, attrValue = {}; + for (i = 0; i < attrs.length; i++) { + attr = attrs[i]; + name = attr.name.replace(':', '-'); + value = attr.value; + if (msie && name == 'href') { + value = decodeURIComponent(element[0].getAttribute(name, 2)); + } + attrValue[name] = value; + } + foreachSorted(attrValue, fn); +} + diff --git a/src/JSON.js b/src/JSON.js new file mode 100644 index 00000000..340b075a --- /dev/null +++ b/src/JSON.js @@ -0,0 +1,105 @@ +array = [].constructor; + +function toJson(obj, pretty){ + var buf = []; + toJsonArray(buf, obj, pretty ? "\n " : null, []); + return buf.join(''); +} + +function toPrettyJson(obj) { + return toJson(obj, true); +} + +function fromJson(json) { + if (!json) return json; + try { + var parser = new Parser(json, true); + var expression = parser.primary(); + parser.assertAllConsumed(); + return expression(); + } catch (e) { + error("fromJson error: ", json, e); + throw e; + } +} + +angular['toJson'] = toJson; +angular['fromJson'] = fromJson; + +function toJsonArray(buf, obj, pretty, stack){ + if (typeof obj == "object") { + if (includes(stack, obj)) { + buf.push("RECURSION"); + return; + } + stack.push(obj); + } + var type = typeof obj; + if (obj === null) { + buf.push("null"); + } else if (type === 'function') { + return; + } else if (type === 'boolean') { + buf.push('' + obj); + } else if (type === 'number') { + if (isNaN(obj)) { + buf.push('null'); + } else { + buf.push('' + obj); + } + } else if (type === 'string') { + return buf.push(angular['String']['quoteUnicode'](obj)); + } else if (type === 'object') { + if (obj instanceof Array) { + buf.push("["); + var len = obj.length; + var sep = false; + for(var i=0; i':function(self, a,b){return a>b;}, + '<=':function(self, a,b){return a<=b;}, + '>=':function(self, a,b){return a>=b;}, + '&&':function(self, a,b){return a&&b;}, + '||':function(self, a,b){return a||b;}, + '&':function(self, a,b){return a&b;}, +// '|':function(self, a,b){return a|b;}, + '|':function(self, a,b){return b(self, a);}, + '!':function(self, a){return !a;} +}; +Lexer.ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + +Lexer.prototype = { + peek: function() { + if (this.index + 1 < this.text.length) { + return this.text.charAt(this.index + 1); + } else { + return false; + } + }, + + parse: function() { + var tokens = this.tokens; + var OPERATORS = Lexer.OPERATORS; + var canStartRegExp = true; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '"' || ch == "'") { + this.readString(ch); + canStartRegExp = true; + } else if (ch == '(' || ch == '[') { + tokens.push({index:this.index, text:ch}); + this.index++; + } else if (ch == '{' ) { + var peekCh = this.peek(); + if (peekCh == ':' || peekCh == '(') { + tokens.push({index:this.index, text:ch + peekCh}); + this.index++; + } else { + tokens.push({index:this.index, text:ch}); + } + this.index++; + canStartRegExp = true; + } else if (ch == ')' || ch == ']' || ch == '}' ) { + tokens.push({index:this.index, text:ch}); + this.index++; + canStartRegExp = false; + } else if ( ch == ':' || ch == '.' || ch == ',' || ch == ';') { + tokens.push({index:this.index, text:ch}); + this.index++; + canStartRegExp = true; + } else if ( canStartRegExp && ch == '/' ) { + this.readRegexp(); + canStartRegExp = false; + } else if ( this.isNumber(ch) ) { + this.readNumber(); + canStartRegExp = false; + } else if (this.isIdent(ch)) { + this.readIdent(); + canStartRegExp = false; + } else if (this.isWhitespace(ch)) { + this.index++; + } else { + var ch2 = ch + this.peek(); + var fn = OPERATORS[ch]; + var fn2 = OPERATORS[ch2]; + if (fn2) { + tokens.push({index:this.index, text:ch2, fn:fn2}); + this.index += 2; + } else if (fn) { + tokens.push({index:this.index, text:ch, fn:fn}); + this.index += 1; + } else { + throw "Lexer Error: Unexpected next character [" + + this.text.substring(this.index) + + "] in expression '" + this.text + + "' at column '" + (this.index+1) + "'."; + } + canStartRegExp = true; + } + } + return tokens; + }, + + isNumber: function(ch) { + return '0' <= ch && ch <= '9'; + }, + + isWhitespace: function(ch) { + return ch == ' ' || ch == '\r' || ch == '\t' || + ch == '\n' || ch == '\v'; + }, + + isIdent: function(ch) { + return 'a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' == ch || ch == '$'; + }, + + readNumber: function() { + var number = ""; + var start = this.index; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '.' || this.isNumber(ch)) { + number += ch; + } else { + break; + } + this.index++; + } + number = 1 * number; + this.tokens.push({index:start, text:number, + fn:function(){return number;}}); + }, + + readIdent: function() { + var ident = ""; + var start = this.index; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '.' || this.isIdent(ch) || this.isNumber(ch)) { + ident += ch; + } else { + break; + } + this.index++; + } + var fn = Lexer.OPERATORS[ident]; + if (!fn) { + fn = getterFn(ident); + fn.isAssignable = ident; + } + this.tokens.push({index:start, text:ident, fn:fn}); + }, + + readString: function(quote) { + var start = this.index; + var dateParseLength = this.dateParseLength; + this.index++; + var string = ""; + var rawString = quote; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + rawString += ch; + if (escape) { + if (ch == 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); + this.index += 4; + string += String.fromCharCode(parseInt(hex, 16)); + } else { + var rep = Lexer.ESCAPE[ch]; + if (rep) { + string += rep; + } else { + string += ch; + } + } + escape = false; + } else if (ch == '\\') { + escape = true; + } else if (ch == quote) { + this.index++; + this.tokens.push({index:start, text:rawString, string:string, + fn:function(){ + return (string.length == dateParseLength) ? + angular['String']['toDate'](string) : string; + }}); + return; + } else { + string += ch; + } + this.index++; + } + throw "Lexer Error: Unterminated quote [" + + this.text.substring(start) + "] starting at column '" + + (start+1) + "' in expression '" + this.text + "'."; + }, + + readRegexp: function(quote) { + var start = this.index; + this.index++; + var regexp = ""; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (escape) { + regexp += ch; + escape = false; + } else if (ch === '\\') { + regexp += ch; + escape = true; + } else if (ch === '/') { + this.index++; + var flags = ""; + if (this.isIdent(this.text.charAt(this.index))) { + this.readIdent(); + flags = this.tokens.pop().text; + } + var compiledRegexp = new RegExp(regexp, flags); + this.tokens.push({index:start, text:regexp, flags:flags, + fn:function(){return compiledRegexp;}}); + return; + } else { + regexp += ch; + } + this.index++; + } + throw "Lexer Error: Unterminated RegExp [" + + this.text.substring(start) + "] starting at column '" + + (start+1) + "' in expression '" + this.text + "'."; + } +}; + +///////////////////////////////////////// + +function Parser(text, parseStrings){ + this.text = text; + this.tokens = new Lexer(text, parseStrings).parse(); + this.index = 0; +} + +Parser.ZERO = function(){ + return 0; +}; + +Parser.prototype = { + error: function(msg, token) { + throw "Token '" + token.text + + "' is " + msg + " at column='" + + (token.index + 1) + "' of expression '" + + this.text + "' starting at '" + this.text.substring(token.index) + "'."; + }, + + peekToken: function() { + if (this.tokens.length === 0) + throw "Unexpected end of expression: " + this.text; + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + var tokens = this.tokens; + if (tokens.length > 0) { + var token = tokens[0]; + var t = token.text; + if (t==e1 || t==e2 || t==e3 || t==e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; + } + } + return false; + }, + + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + this.currentToken = token; + return token; + } + return false; + }, + + consume: function(e1){ + if (!this.expect(e1)) { + var token = this.peek(); + throw "Expecting '" + e1 + "' at column '" + + (token.index+1) + "' in '" + + this.text + "' got '" + + this.text.substring(token.index) + "'."; + } + }, + + _unary: function(fn, right) { + return function(self) { + return fn(self, right(self)); + }; + }, + + _binary: function(left, fn, right) { + return function(self) { + return fn(self, left(self), right(self)); + }; + }, + + hasTokens: function () { + return this.tokens.length > 0; + }, + + assertAllConsumed: function(){ + if (this.tokens.length !== 0) { + throw "Did not understand '" + this.text.substring(this.tokens[0].index) + + "' while evaluating '" + this.text + "'."; + } + }, + + statements: function(){ + var statements = []; + while(true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { + return function (self){ + var value; + for ( var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) + value = statement(self); + } + return value; + }; + } + } + }, + + filterChain: function(){ + var left = this.expression(); + var token; + while(true) { + if ((token = this.expect('|'))) { + left = this._binary(left, token.fn, this.filter()); + } else { + return left; + } + } + }, + + filter: function(){ + return this._pipeFunction(angularFilter); + }, + + validator: function(){ + return this._pipeFunction(angularValidator); + }, + + _pipeFunction: function(fnScope){ + var fn = this.functionIdent(fnScope); + var argsFn = []; + var token; + while(true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); + } else { + var fnInvoke = function(self, input){ + var args = [input]; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + return fn.apply(self, args); + }; + return function(){ + return fnInvoke; + }; + } + } + }, + + expression: function(){ + return this.throwStmt(); + }, + + throwStmt: function(){ + if (this.expect('throw')) { + var throwExp = this.assignment(); + return function (self) { + throw throwExp(self); + }; + } else { + return this.assignment(); + } + }, + + assignment: function(){ + var left = this.logicalOR(); + var token; + if (token = this.expect('=')) { + if (!left.isAssignable) { + throw "Left hand side '" + + this.text.substring(0, token.index) + "' of assignment '" + + this.text.substring(token.index) + "' is not assignable."; + } + var ident = function(){return left.isAssignable;}; + return this._binary(ident, token.fn, this.logicalOR()); + } else { + return left; + } + }, + + logicalOR: function(){ + var left = this.logicalAND(); + var token; + while(true) { + if ((token = this.expect('||'))) { + left = this._binary(left, token.fn, this.logicalAND()); + } else { + return left; + } + } + }, + + logicalAND: function(){ + var left = this.equality(); + var token; + if ((token = this.expect('&&'))) { + left = this._binary(left, token.fn, this.logicalAND()); + } + return left; + }, + + equality: function(){ + var left = this.relational(); + var token; + if ((token = this.expect('==','!='))) { + left = this._binary(left, token.fn, this.equality()); + } + return left; + }, + + relational: function(){ + var left = this.additive(); + var token; + if (token = this.expect('<', '>', '<=', '>=')) { + left = this._binary(left, token.fn, this.relational()); + } + return left; + }, + + additive: function(){ + var left = this.multiplicative(); + var token; + while(token = this.expect('+','-')) { + left = this._binary(left, token.fn, this.multiplicative()); + } + return left; + }, + + multiplicative: function(){ + var left = this.unary(); + var token; + while(token = this.expect('*','/','%')) { + left = this._binary(left, token.fn, this.unary()); + } + return left; + }, + + unary: function(){ + var token; + if (this.expect('+')) { + return this.primary(); + } else if (token = this.expect('-')) { + return this._binary(Parser.ZERO, token.fn, this.unary()); + } else if (token = this.expect('!')) { + return this._unary(token.fn, this.unary()); + } else { + return this.primary(); + } + }, + + functionIdent: function(fnScope) { + var token = this.expect(); + var element = token.text.split('.'); + var instance = fnScope; + var key; + for ( var i = 0; i < element.length; i++) { + key = element[i]; + if (instance) + instance = instance[key]; + } + if (typeof instance != 'function') { + throw "Function '" + token.text + "' at column '" + + (token.index+1) + "' in '" + this.text + "' is not defined."; + } + return instance; + }, + + primary: function() { + var primary; + if (this.expect('(')) { + var expression = this.filterChain(); + this.consume(')'); + primary = expression; + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else if (this.expect('{:')) { + primary = this.closure(false); + } else if (this.expect('{(')) { + primary = this.closure(true); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.error("not a primary expression", token); + } + } + var next; + while (next = this.expect('(', '[', '.')) { + if (next.text === '(') { + primary = this.functionCall(primary); + } else if (next.text === '[') { + primary = this.objectIndex(primary); + } else if (next.text === '.') { + primary = this.fieldAccess(primary); + } else { + throw "IMPOSSIBLE"; + } + } + return primary; + }, + + closure: function(hasArgs) { + var args = []; + if (hasArgs) { + if (!this.expect(')')) { + args.push(this.expect().text); + while(this.expect(',')) { + args.push(this.expect().text); + } + this.consume(')'); + } + this.consume(":"); + } + var statements = this.statements(); + this.consume("}"); + return function(self) { + return function($){ + var scope = createScope(self); + scope['$'] = $; + for ( var i = 0; i < args.length; i++) { + setter(scope, args[i], arguments[i]); + } + return statements(scope); + }; + }; + }, + + fieldAccess: function(object) { + var field = this.expect().text; + var getter = getterFn(field); + var fn = function (self){ + return getter(object(self)); + }; + fn.isAssignable = field; + return fn; + }, + + objectIndex: function(obj) { + var indexFn = this.expression(); + this.consume(']'); + if (this.expect('=')) { + var rhs = this.expression(); + return function (self){ + return obj(self)[indexFn(self)] = rhs(self); + }; + } else { + return function (self){ + var o = obj(self); + var i = indexFn(self); + return (o) ? o[i] : undefined; + }; + } + }, + + functionCall: function(fn) { + var argsFn = []; + if (this.peekToken().text != ')') { + do { + argsFn.push(this.expression()); + } while (this.expect(',')); + } + this.consume(')'); + return function (self){ + var args = []; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + var fnPtr = fn(self); + if (typeof fnPtr === 'function') { + return fnPtr.apply(self, args); + } else { + throw "Expression '" + fn.isAssignable + "' is not a function."; + } + }; + }, + + // This is used with json array declaration + arrayDeclaration: function () { + var elementFns = []; + if (this.peekToken().text != ']') { + do { + elementFns.push(this.expression()); + } while (this.expect(',')); + } + this.consume(']'); + return function (self){ + var array = []; + for ( var i = 0; i < elementFns.length; i++) { + array.push(elementFns[i](self)); + } + return array; + }; + }, + + object: function () { + var keyValues = []; + if (this.peekToken().text != '}') { + do { + var token = this.expect(), + key = token.string || token.text; + this.consume(":"); + var value = this.expression(); + keyValues.push({key:key, value:value}); + } while (this.expect(',')); + } + this.consume('}'); + return function (self){ + var object = {}; + for ( var i = 0; i < keyValues.length; i++) { + var keyValue = keyValues[i]; + var value = keyValue.value(self); + object[keyValue.key] = value; + } + return object; + }; + }, + + entityDeclaration: function () { + var decl = []; + while(this.hasTokens()) { + decl.push(this.entityDecl()); + if (!this.expect(';')) { + this.assertAllConsumed(); + } + } + return function (self){ + var code = ""; + for ( var i = 0; i < decl.length; i++) { + code += decl[i](self); + } + return code; + }; + }, + + entityDecl: function () { + var entity = this.expect().text; + var instance; + var defaults; + if (this.expect('=')) { + instance = entity; + entity = this.expect().text; + } + if (this.expect(':')) { + defaults = this.primary()(null); + } + return function(self) { + var Entity = self.datastore.entity(entity, defaults); + setter(self, entity, Entity); + if (instance) { + var document = Entity(); + document['$$anchor'] = instance; + setter(self, instance, document); + return "$anchor." + instance + ":{" + + instance + "=" + entity + ".load($anchor." + instance + ");" + + instance + ".$$anchor=" + angular['String']['quote'](instance) + ";" + + "};"; + } else { + return ""; + } + }; + }, + + watch: function () { + var decl = []; + while(this.hasTokens()) { + decl.push(this.watchDecl()); + if (!this.expect(';')) { + this.assertAllConsumed(); + } + } + this.assertAllConsumed(); + return function (self){ + for ( var i = 0; i < decl.length; i++) { + var d = decl[i](self); + self.addListener(d.name, d.fn); + } + }; + }, + + watchDecl: function () { + var anchorName = this.expect().text; + this.consume(":"); + var expression; + if (this.peekToken().text == '{') { + this.consume("{"); + expression = this.statements(); + this.consume("}"); + } else { + expression = this.expression(); + } + return function(self) { + return {name:anchorName, fn:expression}; + }; + } +}; + diff --git a/src/Resource.js b/src/Resource.js new file mode 100644 index 00000000..ba460c30 --- /dev/null +++ b/src/Resource.js @@ -0,0 +1,140 @@ +function Route(template, defaults) { + this.template = template = template + '#'; + this.defaults = defaults || {}; + var urlParams = this.urlParams = {}; + foreach(template.split(/\W/), function(param){ + if (param && template.match(new RegExp(":" + param + "\\W"))) { + urlParams[param] = true; + } + }); +} + +Route.prototype = { + url: function(params) { + var path = []; + var self = this; + var url = this.template; + params = params || {}; + foreach(this.urlParams, function(_, urlParam){ + var value = params[urlParam] || self.defaults[urlParam] || ""; + url = url.replace(new RegExp(":" + urlParam + "(\\W)"), value + "$1"); + }); + url = url.replace(/\/?#$/, ''); + var query = []; + foreachSorted(params, function(value, key){ + if (!self.urlParams[key]) { + query.push(encodeURI(key) + '=' + encodeURI(value)); + } + }); + return url + (query.length ? '?' + query.join('&') : ''); + } +}; + +function ResourceFactory(xhr) { + this.xhr = xhr; +} + +ResourceFactory.DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} +}; + +ResourceFactory.prototype = { + route: function(url, paramDefaults, actions){ + var self = this; + var route = new Route(url); + actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions); + function extractParams(data){ + var ids = {}; + foreach(paramDefaults || {}, function(value, key){ + ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; + }); + return ids; + } + + function Resource(value){ + copy(value || {}, this); + } + + foreach(actions, function(action, name){ + var isGet = action.method == 'GET'; + var isPost = action.method == 'POST'; + Resource[name] = function (a1, a2, a3) { + var params = {}; + var data; + var callback = noop; + switch(arguments.length) { + case 3: callback = a3; + case 2: + if (isFunction(a2)) { + callback = a2; + } else { + params = a1; + data = a2; + break; + } + case 1: + if (isFunction(a1)) callback = a1; + else if (isPost) data = a1; + else params = a1; + break; + case 0: break; + default: + throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; + } + + var value = action.isArray ? [] : new Resource(data;) + self.xhr( + action.method, + route.url(extend({}, action.params || {}, extractParams(data), params)), + data, + function(status, response, clear) { + if (status == 200) { + if (action.isArray) { + if (action.cacheThenRetrieve) + value = []; + foreach(response, function(item){ + value.push(new Resource(item)); + }); + } else { + copy(response, value); + } + (callback||noop)(value); + } else { + throw {status: status, response:response, message: status + ": " + response}; + } + }, + action.cacheThenRetrieve + ); + return value; + }; + + Resource.bind = function(additionalParamDefaults){ + return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + if (!isGet) { + Resource.prototype['$' + name] = function(a1, a2){ + var params = {}; + var callback = noop; + switch(arguments.length) { + case 2: params = a1; callback = a2; + case 1: if (typeof a1 == 'function') callback = a1; else params = a1; + case 0: break; + default: + throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; + } + var self = this; + Resource[name](params, this, function(response){ + copy(response, self); + callback(self); + }); + }; + } + }); + return Resource; + } +}; diff --git a/src/Scope.js b/src/Scope.js new file mode 100644 index 00000000..637fc25e --- /dev/null +++ b/src/Scope.js @@ -0,0 +1,224 @@ +function getter(instance, path, unboundFn) { + if (!path) return instance; + var element = path.split('.'); + var key; + var lastInstance = instance; + var len = element.length; + for ( var i = 0; i < len; i++) { + key = element[i]; + if (!key.match(/^[\$\w][\$\w\d]*$/)) + throw "Expression '" + path + "' is not a valid expression for accesing variables."; + if (instance) { + lastInstance = instance; + instance = instance[key]; + } + if (isUndefined(instance) && key.charAt(0) == '$') { + var type = angular['Global']['typeOf'](lastInstance); + type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; + var fn = type ? type[[key.substring(1)]] : undefined; + if (fn) { + instance = bind(lastInstance, fn, lastInstance); + return instance; + } + } + } + if (!unboundFn && isFunction(instance) && !instance['$$factory']) { + return bind(lastInstance, instance); + } + return instance; +} + +function setter(instance, path, value){ + var element = path.split('.'); + for ( var i = 0; element.length > 1; i++) { + var key = element.shift(); + var newInstance = instance[key]; + if (!newInstance) { + newInstance = {}; + instance[key] = newInstance; + } + instance = newInstance; + } + instance[element.shift()] = value; + return value; +} + +/////////////////////////////////// + +var getterFnCache = {}; +function getterFn(path){ + var fn = getterFnCache[path]; + if (fn) return fn; + + var code = 'function (self){\n'; + code += ' var last, fn, type;\n'; + foreach(path.split('.'), function(key) { + key = (key == 'this') ? '["this"]' : '.' + key; + code += ' if(!self) return self;\n'; + code += ' last = self;\n'; + code += ' self = self' + key + ';\n'; + code += ' if(typeof self == "function") \n'; + code += ' self = function(){ return last'+key+'.apply(last, arguments); };\n'; + if (key.charAt(1) == '$') { + // special code for super-imposed functions + var name = key.substr(2); + code += ' if(!self) {\n'; + code += ' type = angular.Global.typeOf(last);\n'; + code += ' fn = (angular[type.charAt(0).toUpperCase() + type.substring(1)]||{})["' + name + '"];\n'; + code += ' if (fn)\n'; + code += ' self = function(){ return fn.apply(last, [last].concat(slice.call(arguments, 0, arguments.length))); };\n'; + code += ' }\n'; + } + }); + code += ' return self;\n}'; + fn = eval('(' + code + ')'); + fn.toString = function(){ return code; }; + + return getterFnCache[path] = fn; +} + +/////////////////////////////////// + +var compileCache = {}; +function expressionCompile(exp){ + if (isFunction(exp)) return exp; + var fn = compileCache[exp]; + if (!fn) { + var parser = new Parser(exp); + var fnSelf = parser.statements(); + parser.assertAllConsumed(); + fn = compileCache[exp] = extend( + function(){ return fnSelf(this);}, + {fnSelf: fnSelf}); + } + return fn; +} + +function rethrow(e) { throw e; } +function errorHandlerFor(element, error) { + elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error); +} + +var scopeId = 0; +function createScope(parent, services, existing) { + function Parent(){} + function API(){} + function Behavior(){} + + var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing); + + parent = Parent.prototype = (parent || {}); + api = API.prototype = new Parent(); + behavior = Behavior.prototype = new API(); + instance = new Behavior(); + + extend(api, { + 'this': instance, + $id: (scopeId++), + $parent: parent, + $bind: bind(instance, bind, instance), + $get: bind(instance, getter, instance), + $set: bind(instance, setter, instance), + + $eval: function $eval(exp) { + if (exp !== undefined) { + return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length)); + } else { + for ( var i = 0, iSize = evalLists.sorted.length; i < iSize; i++) { + for ( var queue = evalLists.sorted[i], + jSize = queue.length, + j= 0; j < jSize; j++) { + instance.$tryEval(queue[j].fn, queue[j].handler); + } + } + } + }, + + $tryEval: function (expression, exceptionHandler) { + try { + return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length)); + } catch (e) { + error(e); + if (isFunction(exceptionHandler)) { + exceptionHandler(e); + } else if (exceptionHandler) { + errorHandlerFor(exceptionHandler, e); + } else if (isFunction(instance.$exceptionHandler)) { + instance.$exceptionHandler(e); + } + } + }, + + $watch: function(watchExp, listener, exceptionHandler) { + var watch = expressionCompile(watchExp), + last; + function watcher(){ + var value = watch.call(instance), + lastValue = last; + if (last !== value) { + last = value; + instance.$tryEval(listener, exceptionHandler, value, lastValue); + } + } + instance.$onEval(PRIORITY_WATCH, watcher); + watcher(); + }, + + $onEval: function(priority, expr, exceptionHandler){ + if (!isNumber(priority)) { + exceptionHandler = expr; + expr = priority; + priority = 0; + } + var evalList = evalLists[priority]; + if (!evalList) { + evalList = evalLists[priority] = []; + evalList.priority = priority; + evalLists.sorted.push(evalList); + evalLists.sorted.sort(function(a,b){return a.priority-b.priority;}); + } + evalList.push({ + fn: expressionCompile(expr), + handler: exceptionHandler + }); + }, + + $become: function(Class) { + // remove existing + foreach(behavior, function(value, key){ delete behavior[key]; }); + foreach((Class || noop).prototype, function(fn, name){ + behavior[name] = bind(instance, fn); + }); + (Class || noop).call(instance); + } + + }); + + if (!parent.$root) { + api.$root = instance; + api.$parent = instance; + } + + function inject(name){ + var service = servicesCache[name], factory, args = []; + if (isUndefined(service)) { + factory = services[name]; + if (!isFunction(factory)) + throw "Don't know how to inject '" + name + "'."; + foreach(factory.inject, function(dependency){ + args.push(inject(dependency)); + }); + servicesCache[name] = service = factory.apply(instance, args); + } + return service; + } + + foreach(services, function(_, name){ + var service = inject(name); + if (service) { + setter(instance, name, service); + } + }); + + return instance; +} diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js new file mode 100644 index 00000000..90e1104e --- /dev/null +++ b/src/angular-bootstrap.js @@ -0,0 +1,70 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(previousOnLoad){ + var filename = /(.*)\/angular-(.*).js(#(.*))?/, + scripts = document.getElementsByTagName("SCRIPT"), + serverPath, + config, + match; + for(var j = 0; j < scripts.length; j++) { + match = (scripts[j].src || "").match(filename); + if (match) { + serverPath = match[1]; + config = match[4]; + } + } + + function addScript(file){ + document.write(''); + } + + addScript("/Angular.js"); + addScript("/JSON.js"); + addScript("/Compiler.js"); + addScript("/Scope.js"); + addScript("/jqLite.js"); + addScript("/Parser.js"); + addScript("/Resource.js"); + addScript("/Browser.js"); + addScript("/AngularPublic.js"); + + // Extension points + addScript("/services.js"); + addScript("/apis.js"); + addScript("/filters.js"); + addScript("/formatters.js"); + addScript("/validators.js"); + addScript("/directives.js"); + addScript("/markups.js"); + addScript("/widgets.js"); + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularInit(parseKeyValue(config)); + }; + +})(window.onload); + diff --git a/src/angular.prefix b/src/angular.prefix new file mode 100644 index 00000000..a1b4e151 --- /dev/null +++ b/src/angular.prefix @@ -0,0 +1,24 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window, document, previousOnLoad){ diff --git a/src/angular.suffix b/src/angular.suffix new file mode 100644 index 00000000..36d73df2 --- /dev/null +++ b/src/angular.suffix @@ -0,0 +1,9 @@ + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularInit(parseKeyValue(angularJsConfig(document))); + }; + +})(window, document, window.onload); diff --git a/src/apis.js b/src/apis.js new file mode 100644 index 00000000..306d0ce8 --- /dev/null +++ b/src/apis.js @@ -0,0 +1,338 @@ +var angularGlobal = { + 'typeOf':function(obj){ + if (obj === null) return "null"; + var type = typeof obj; + if (type == "object") { + if (obj instanceof Array) return "array"; + if (obj instanceof Date) return "date"; + if (obj.nodeType == 1) return "element"; + } + return type; + } +}; + +var angularCollection = { + 'size': size +}; +var angularObject = { + 'extend': extend +}; +var angularArray = { + 'indexOf': indexOf, + 'include': includes, + 'includeIf':function(array, value, condition) { + var index = indexOf(array, value); + if (condition) { + if (index == -1) + array.push(value); + } else { + array.splice(index, 1); + } + return array; + }, + 'sum':function(array, expression) { + var fn = angular['Function']['compile'](expression); + var sum = 0; + for (var i = 0; i < array.length; i++) { + var value = 1 * fn(array[i]); + if (!isNaN(value)){ + sum += value; + } + } + return sum; + }, + 'remove':function(array, value) { + var index = indexOf(array, value); + if (index >=0) + array.splice(index, 1); + return value; + }, + 'find':function(array, condition, defaultValue) { + if (!condition) return undefined; + var fn = angular['Function']['compile'](condition); + foreach(array, function($){ + if (fn($)){ + defaultValue = $; + return true; + } + }); + return defaultValue; + }, + 'findById':function(array, id) { + return angular.Array.find(array, function($){return $.$id == id;}, null); + }, + 'filter':function(array, expression) { + var predicates = []; + predicates.check = function(value) { + for (var j = 0; j < predicates.length; j++) { + if(!predicates[j](value)) { + return false; + } + } + return true; + }; + var search = function(obj, text){ + if (text.charAt(0) === '!') { + return !search(obj, text.substr(1)); + } + switch (typeof obj) { + case "boolean": + case "number": + case "string": + return ('' + obj).toLowerCase().indexOf(text) > -1; + case "object": + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } + } + return false; + default: + return false; + } + }; + switch (typeof expression) { + case "boolean": + case "number": + case "string": + expression = {$:expression}; + case "object": + for (var key in expression) { + if (key == '$') { + (function(){ + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(value, text); + }); + })(); + } else { + (function(){ + var path = key; + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(getter(value, path), text); + }); + })(); + } + } + break; + case "function": + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + }, + 'add':function(array, value) { + array.push(isUndefined(value)? {} : value); + return array; + }, + 'count':function(array, condition) { + if (!condition) return array.length; + var fn = angular['Function']['compile'](condition), count = 0; + foreach(array, function(value){ + if (fn(value)) { + count ++; + } + }); + return count; + }, + 'orderBy':function(array, expression, descend) { + function reverse(comp, descending) { + return toBoolean(descending) ? + function(a,b){return comp(b,a);} : comp; + } + function compare(v1, v2){ + var t1 = typeof v1; + var t2 = typeof v2; + if (t1 == t2) { + if (t1 == "string") v1 = v1.toLowerCase(); + if (t1 == "string") v2 = v2.toLowerCase(); + if (v1 === v2) return 0; + return v1 < v2 ? -1 : 1; + } else { + return t1 < t2 ? -1 : 1; + } + } + expression = isArray(expression) ? expression: [expression]; + expression = map(expression, function($){ + var descending = false; + if (typeof $ == "string" && ($.charAt(0) == '+' || $.charAt(0) == '-')) { + descending = $.charAt(0) == '-'; + $ = $.substring(1); + } + var get = $ ? expressionCompile($).fnSelf : identity; + return reverse(function(a,b){ + return compare(get(a),get(b)); + }, descending); + }); + var comparator = function(o1, o2){ + for ( var i = 0; i < expression.length; i++) { + var comp = expression[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + }; + var arrayCopy = []; + for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } + return arrayCopy.sort(reverse(comparator, descend)); + }, + 'orderByToggle':function(predicate, attribute) { + var STRIP = /^([+|-])?(.*)/; + var ascending = false; + var index = -1; + foreach(predicate, function($, i){ + if (index == -1) { + if ($ == attribute) { + ascending = true; + index = i; + return true; + } + if (($.charAt(0)=='+'||$.charAt(0)=='-') && $.substring(1) == attribute) { + ascending = $.charAt(0) == '+'; + index = i; + return true; + } + } + }); + if (index >= 0) { + predicate.splice(index, 1); + } + predicate.unshift((ascending ? "-" : "+") + attribute); + return predicate; + }, + 'orderByDirection':function(predicate, attribute, ascend, descend) { + ascend = ascend || 'ng-ascend'; + descend = descend || 'ng-descend'; + var att = predicate[0] || ''; + var direction = true; + if (att.charAt(0) == '-') { + att = att.substring(1); + direction = false; + } else if(att.charAt(0) == '+') { + att = att.substring(1); + } + return att == attribute ? (direction ? ascend : descend) : ""; + }, + 'merge':function(array, index, mergeValue) { + var value = array[index]; + if (!value) { + value = {}; + array[index] = value; + } + merge(mergeValue, value); + return array; + } +}; + +var angularString = { + 'quote':function(string) { + return '"' + string.replace(/\\/g, '\\\\'). + replace(/"/g, '\\"'). + replace(/\n/g, '\\n'). + replace(/\f/g, '\\f'). + replace(/\r/g, '\\r'). + replace(/\t/g, '\\t'). + replace(/\v/g, '\\v') + + '"'; + }, + 'quoteUnicode':function(string) { + var str = angular['String']['quote'](string); + var chars = []; + for ( var i = 0; i < str.length; i++) { + var ch = str.charCodeAt(i); + if (ch < 128) { + chars.push(str.charAt(i)); + } else { + var encode = "000" + ch.toString(16); + chars.push("\\u" + encode.substring(encode.length - 4)); + } + } + return chars.join(''); + }, + 'toDate':function(string){ + var match; + if (typeof string == 'string' && + (match = string.match(/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/))){ + var date = new Date(0); + date.setUTCFullYear(match[1], match[2] - 1, match[3]); + date.setUTCHours(match[4], match[5], match[6], 0); + return date; + } + return string; + } +}; + +var angularDate = { + 'toString':function(date){ + function pad(n) { return n < 10 ? "0" + n : n; } + return !date ? date : + date.getUTCFullYear() + '-' + + pad(date.getUTCMonth() + 1) + '-' + + pad(date.getUTCDate()) + 'T' + + pad(date.getUTCHours()) + ':' + + pad(date.getUTCMinutes()) + ':' + + pad(date.getUTCSeconds()) + 'Z' ; + } + }; + +var angularFunction = { + 'compile':function(expression) { + if (isFunction(expression)){ + return expression; + } else if (expression){ + return expressionCompile(expression).fnSelf; + } else { + return identity; + } + } +}; + +function defineApi(dst, chain, underscoreNames){ + if (_) { + var lastChain = _.last(chain); + foreach(underscoreNames, function(name){ + lastChain[name] = _[name]; + }); + } + angular[dst] = angular[dst] || {}; + foreach(chain, function(parent){ + extend(angular[dst], parent); + }); +} +defineApi('Global', [angularGlobal], + ['extend', 'clone','isEqual', + 'isElement', 'isArray', 'isFunction', 'isUndefined']); +defineApi('Collection', [angularGlobal, angularCollection], + ['each', 'map', 'reduce', 'reduceRight', 'detect', + 'select', 'reject', 'all', 'any', 'include', + 'invoke', 'pluck', 'max', 'min', 'sortBy', + 'sortedIndex', 'toArray', 'size']); +defineApi('Array', [angularGlobal, angularCollection, angularArray], + ['first', 'last', 'compact', 'flatten', 'without', + 'uniq', 'intersect', 'zip', 'indexOf', 'lastIndexOf']); +defineApi('Object', [angularGlobal, angularCollection, angularObject], + ['keys', 'values']); +defineApi('String', [angularGlobal, angularString], []); +defineApi('Date', [angularGlobal, angularDate], []); +//IE bug +angular['Date']['toString'] = angularDate['toString']; +defineApi('Function', [angularGlobal, angularCollection, angularFunction], + ['bind', 'bindAll', 'delay', 'defer', 'wrap', 'compose']); diff --git a/src/delete/Binder.js b/src/delete/Binder.js new file mode 100644 index 00000000..9fc32513 --- /dev/null +++ b/src/delete/Binder.js @@ -0,0 +1,356 @@ +function Binder(doc, widgetFactory, datastore, location, config) { + this.doc = doc; + this.location = location; + this.datastore = datastore; + this.anchor = {}; + this.widgetFactory = widgetFactory; + this.config = config || {}; + this.updateListeners = []; +} + +Binder.parseBindings = function(string) { + var results = []; + var lastIndex = 0; + var index; + while((index = string.indexOf('{{', lastIndex)) > -1) { + if (lastIndex < index) + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + + index = string.indexOf('}}', index); + index = index < 0 ? string.length : index + 2; + + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + } + if (lastIndex != string.length) + results.push(string.substr(lastIndex, string.length - lastIndex)); + return results.length === 0 ? [ string ] : results; +}; + +Binder.hasBinding = function(string) { + var bindings = Binder.parseBindings(string); + return bindings.length > 1 || Binder.binding(bindings[0]) !== null; +}; + +Binder.binding = function(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +}; + + +Binder.prototype = { + parseQueryString: function(query) { + var params = {}; + query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, + function (match, left, right) { + if (left) params[decodeURIComponent(left)] = decodeURIComponent(right); + }); + return params; + }, + + parseAnchor: function() { + var self = this, url = this.location['get']() || ""; + + var anchorIndex = url.indexOf('#'); + if (anchorIndex < 0) return; + var anchor = url.substring(anchorIndex + 1); + + var anchorQuery = this.parseQueryString(anchor); + foreach(self.anchor, function(newValue, key) { + delete self.anchor[key]; + }); + foreach(anchorQuery, function(newValue, key) { + self.anchor[key] = newValue; + }); + }, + + onUrlChange: function() { + this.parseAnchor(); + this.updateView(); + }, + + updateAnchor: function() { + var url = this.location['get']() || ""; + var anchorIndex = url.indexOf('#'); + if (anchorIndex > -1) + url = url.substring(0, anchorIndex); + url += "#"; + var sep = ''; + for (var key in this.anchor) { + var value = this.anchor[key]; + if (typeof value === 'undefined' || value === null) { + delete this.anchor[key]; + } else { + url += sep + encodeURIComponent(key); + if (value !== true) + url += "=" + encodeURIComponent(value); + sep = '&'; + } + } + this.location['set'](url); + return url; + }, + + updateView: function() { + var start = new Date().getTime(); + var scope = jQuery(this.doc).scope(); + scope.clearInvalid(); + scope.updateView(); + var end = new Date().getTime(); + this.updateAnchor(); + foreach(this.updateListeners, function(fn) {fn();}); + }, + + docFindWithSelf: function(exp){ + var doc = jQuery(this.doc); + var selection = doc.find(exp); + if (doc.is(exp)){ + selection = selection.andSelf(); + } + return selection; + }, + + executeInit: function() { + this.docFindWithSelf("[ng-init]").each(function() { + var jThis = jQuery(this); + var scope = jThis.scope(); + try { + scope.eval(jThis.attr('ng-init')); + } catch (e) { + alert("EVAL ERROR:\n" + jThis.attr('ng-init') + '\n' + toJson(e, true)); + } + }); + }, + + entity: function (scope) { + var self = this; + this.docFindWithSelf("[ng-entity]").attr("ng-watch", function() { + try { + var jNode = jQuery(this); + var decl = scope.entity(jNode.attr("ng-entity"), self.datastore); + return decl + (jNode.attr('ng-watch') || ""); + } catch (e) { + log(e); + alert(e); + } + }); + }, + + compile: function() { + var jNode = jQuery(this.doc); + if (this.config['autoSubmit']) { + var submits = this.docFindWithSelf(":submit").not("[ng-action]"); + submits.attr("ng-action", "$save()"); + submits.not(":disabled").not("ng-bind-attr").attr("ng-bind-attr", '{disabled:"{{$invalidWidgets}}"}'); + } + this.precompile(this.doc)(this.doc, jNode.scope(), ""); + this.docFindWithSelf("a[ng-action]").live('click', function (event) { + var jNode = jQuery(this); + var scope = jNode.scope(); + try { + scope.eval(jNode.attr('ng-action')); + jNode.removeAttr('ng-error'); + jNode.removeClass("ng-exception"); + } catch (e) { + jNode.addClass("ng-exception"); + jNode.attr('ng-error', toJson(e, true)); + } + scope.get('$updateView')(); + return false; + }); + }, + + translateBinding: function(node, parentPath, factories) { + var path = parentPath.concat(); + var offset = path.pop(); + var parts = Binder.parseBindings(node.nodeValue); + if (parts.length > 1 || Binder.binding(parts[0])) { + var parent = node.parentNode; + if (isLeafNode(parent)) { + parent.setAttribute('ng-bind-template', node.nodeValue); + factories.push({path:path, fn:function(node, scope, prefix) { + return new BindUpdater(node, node.getAttribute('ng-bind-template')); + }}); + } else { + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + var binding = Binder.binding(part); + var newNode; + if (binding) { + newNode = document.createElement("span"); + var jNewNode = jQuery(newNode); + jNewNode.attr("ng-bind", binding); + if (i === 0) { + factories.push({path:path.concat(offset + i), fn:this.ng_bind}); + } + } else if (msie && part.charAt(0) == ' ') { + newNode = document.createElement("span"); + newNode.innerHTML = ' ' + part.substring(1); + } else { + newNode = document.createTextNode(part); + } + parent.insertBefore(newNode, node); + } + } + parent.removeChild(node); + } + }, + + precompile: function(root) { + var factories = []; + this.precompileNode(root, [], factories); + return function (template, scope, prefix) { + var len = factories.length; + for (var i = 0; i < len; i++) { + var factory = factories[i]; + var node = template; + var path = factory.path; + for (var j = 0; j < path.length; j++) { + node = node.childNodes[path[j]]; + } + try { + scope.addWidget(factory.fn(node, scope, prefix)); + } catch (e) { + alert(e); + } + } + }; + }, + + precompileNode: function(node, path, factories) { + var nodeType = node.nodeType; + if (nodeType == Node.TEXT_NODE) { + this.translateBinding(node, path, factories); + return; + } else if (nodeType != Node.ELEMENT_NODE && nodeType != Node.DOCUMENT_NODE) { + return; + } + + if (!node.getAttribute) return; + var nonBindable = node.getAttribute('ng-non-bindable'); + if (nonBindable || nonBindable === "") return; + + var attributes = node.attributes; + if (attributes) { + var bindings = node.getAttribute('ng-bind-attr'); + node.removeAttribute('ng-bind-attr'); + bindings = bindings ? fromJson(bindings) : {}; + var attrLen = attributes.length; + for (var i = 0; i < attrLen; i++) { + var attr = attributes[i]; + var attrName = attr.name; + // http://www.glennjones.net/Post/809/getAttributehrefbug.htm + var attrValue = msie && attrName == 'href' ? + decodeURI(node.getAttribute(attrName, 2)) : attr.value; + if (Binder.hasBinding(attrValue)) { + bindings[attrName] = attrValue; + } + } + var json = toJson(bindings); + if (json.length > 2) { + node.setAttribute("ng-bind-attr", json); + } + } + + if (!node.getAttribute) log(node); + var repeaterExpression = node.getAttribute('ng-repeat'); + if (repeaterExpression) { + node.removeAttribute('ng-repeat'); + var precompiled = this.precompile(node); + var view = document.createComment("ng-repeat: " + repeaterExpression); + var parentNode = node.parentNode; + parentNode.insertBefore(view, node); + parentNode.removeChild(node); + function template(childScope, prefix, i) { + var clone = jQuery(node).clone(); + clone.css('display', ''); + clone.attr('ng-repeat-index', "" + i); + clone.data('scope', childScope); + precompiled(clone[0], childScope, prefix + i + ":"); + return clone; + } + factories.push({path:path, fn:function(node, scope, prefix) { + return new RepeaterUpdater(jQuery(node), repeaterExpression, template, prefix); + }}); + return; + } + + if (node.getAttribute('ng-eval')) factories.push({path:path, fn:this.ng_eval}); + if (node.getAttribute('ng-bind')) factories.push({path:path, fn:this.ng_bind}); + if (node.getAttribute('ng-bind-attr')) factories.push({path:path, fn:this.ng_bind_attr}); + if (node.getAttribute('ng-hide')) factories.push({path:path, fn:this.ng_hide}); + if (node.getAttribute('ng-show')) factories.push({path:path, fn:this.ng_show}); + if (node.getAttribute('ng-class')) factories.push({path:path, fn:this.ng_class}); + if (node.getAttribute('ng-class-odd')) factories.push({path:path, fn:this.ng_class_odd}); + if (node.getAttribute('ng-class-even')) factories.push({path:path, fn:this.ng_class_even}); + if (node.getAttribute('ng-style')) factories.push({path:path, fn:this.ng_style}); + if (node.getAttribute('ng-watch')) factories.push({path:path, fn:this.ng_watch}); + var nodeName = node.nodeName; + if ((nodeName == 'INPUT' ) || + nodeName == 'TEXTAREA' || + nodeName == 'SELECT' || + nodeName == 'BUTTON') { + var self = this; + factories.push({path:path, fn:function(node, scope, prefix) { + node.name = prefix + node.name.split(":").pop(); + return self.widgetFactory.createController(jQuery(node), scope); + }}); + } + if (nodeName == 'OPTION') { + var html = jQuery('' + + '' + + '' + + '' + + ''); +}; + +extend(FileController.prototype, { + 'cancel': noop, + 'complete': noop, + 'httpStatus': function(status) { + alert("httpStatus:" + this.scopeName + " status:" + status); + }, + 'ioError': function() { + alert("ioError:" + this.scopeName); + }, + 'open': function() { + alert("open:" + this.scopeName); + }, + 'progress':noop, + 'securityError': function() { + alert("securityError:" + this.scopeName); + }, + 'uploadCompleteData': function(data) { + var value = fromJson(data); + value.url = this.attachmentsPath + '/' + value.id + '/' + value.text; + this.view.find("input").attr('checked', true); + var scope = this.view.scope(); + this.value = value; + this.updateModel(scope); + this.value = null; + }, + 'select': function(name, size, type) { + this.name = name; + this.view.find("a").text(name).attr('href', name); + this.view.find("span").text(angular['filter']['bytes'](size)); + this.upload(); + }, + + updateModel: function(scope) { + var isChecked = this.view.find("input").attr('checked'); + var value = isChecked ? this.value : null; + if (this.lastValue === value) { + return false; + } else { + scope.set(this.scopeName, value); + return true; + } + }, + + updateView: function(scope) { + var modelValue = scope.get(this.scopeName); + if (modelValue && this.value !== modelValue) { + this.value = modelValue; + this.view.find("a"). + attr("href", this.value.url). + text(this.value.text); + this.view.find("span").text(angular['filter']['bytes'](this.value.size)); + } + this.view.find("input").attr('checked', !!modelValue); + }, + + upload: function() { + if (this.name) { + this.uploader['uploadFile'](this.attachmentsPath); + } + } +}); + +/////////////////////// +// NullController +/////////////////////// +function NullController(view) {this.view = view;}; +NullController.prototype = { + updateModel: function() { return true; }, + updateView: noop +}; +NullController.instance = new NullController(); + + +/////////////////////// +// ButtonController +/////////////////////// +var ButtonController = NullController; + +/////////////////////// +// TextController +/////////////////////// +function TextController(view, exp, formatter) { + this.view = view; + this.formatter = formatter; + this.exp = exp; + this.validator = view.getAttribute('ng-validate'); + this.required = typeof view.attributes['ng-required'] != "undefined"; + this.lastErrorText = null; + this.lastValue = undefined; + this.initialValue = this.formatter['parse'](view.value); + var widget = view.getAttribute('ng-widget'); + if (widget === 'datepicker') { + jQuery(view).datepicker(); + } +}; + +TextController.prototype = { + updateModel: function(scope) { + var value = this.formatter['parse'](this.view.value); + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } + }, + + updateView: function(scope) { + var view = this.view; + var value = scope.get(this.exp); + if (typeof value === "undefined") { + value = this.initialValue; + scope.setEval(this.exp, value); + } + value = value ? value : ''; + if (!_(this.lastValue).isEqual(value)) { + view.value = this.formatter['format'](value); + this.lastValue = value; + } + + var isValidationError = false; + view.removeAttribute('ng-error'); + if (this.required) { + isValidationError = !(value && $.trim("" + value).length > 0); + } + var errorText = isValidationError ? "Required Value" : null; + if (!isValidationError && this.validator && value) { + errorText = scope.validate(this.validator, value, view); + isValidationError = !!errorText; + } + if (this.lastErrorText !== errorText) { + this.lastErrorText = isValidationError; + if (errorText && isVisible(view)) { + view.setAttribute('ng-error', errorText); + scope.markInvalid(this); + } + jQuery(view).toggleClass('ng-validation-error', isValidationError); + } + } +}; + +/////////////////////// +// CheckboxController +/////////////////////// +function CheckboxController(view, exp, formatter) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.formatter = formatter; + this.initialValue = this.formatter['parse'](view.checked ? view.value : ""); +}; + +CheckboxController.prototype = { + updateModel: function(scope) { + var input = this.view; + var value = input.checked ? input.value : ''; + value = this.formatter['parse'](value); + value = this.formatter['format'](value); + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, this.formatter['parse'](value)); + this.lastValue = value; + return true; + } + }, + + updateView: function(scope) { + var input = this.view; + var value = scope.eval(this.exp); + if (typeof value === "undefined") { + value = this.initialValue; + scope.setEval(this.exp, value); + } + input.checked = this.formatter['parse'](input.value) == value; + } +}; + +/////////////////////// +// SelectController +/////////////////////// +function SelectController(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = view.value; +}; + +SelectController.prototype = { + updateModel: function(scope) { + var input = this.view; + if (input.selectedIndex < 0) { + scope.setEval(this.exp, null); + } else { + var value = this.view.value; + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } + } + }, + + updateView: function(scope) { + var input = this.view; + var value = scope.get(this.exp); + if (typeof value === 'undefined') { + value = this.initialValue; + scope.setEval(this.exp, value); + } + if (value !== this.lastValue) { + input.value = value ? value : ""; + this.lastValue = value; + } + } +}; + +/////////////////////// +// MultiSelectController +/////////////////////// +function MultiSelectController(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = this.selected(); +}; + +MultiSelectController.prototype = { + selected: function () { + var value = []; + var options = this.view.options; + for ( var i = 0; i < options.length; i++) { + var option = options[i]; + if (option.selected) { + value.push(option.value); + } + } + return value; + }, + + updateModel: function(scope) { + var value = this.selected(); + // TODO: This is wrong! no caching going on here as we are always comparing arrays + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } + }, + + updateView: function(scope) { + var input = this.view; + var selected = scope.get(this.exp); + if (typeof selected === "undefined") { + selected = this.initialValue; + scope.setEval(this.exp, selected); + } + if (selected !== this.lastValue) { + var options = input.options; + for ( var i = 0; i < options.length; i++) { + var option = options[i]; + option.selected = _.include(selected, option.value); + } + this.lastValue = selected; + } + } +}; + +/////////////////////// +// RadioController +/////////////////////// +function RadioController(view, exp) { + this.view = view; + this.exp = exp; + this.lastChecked = undefined; + this.lastValue = undefined; + this.inputValue = view.value; + this.initialValue = view.checked ? view.value : null; +}; + +RadioController.prototype = { + updateModel: function(scope) { + var input = this.view; + if (this.lastChecked) { + return false; + } else { + input.checked = true; + this.lastValue = scope.setEval(this.exp, this.inputValue); + this.lastChecked = true; + return true; + } + }, + + updateView: function(scope) { + var input = this.view; + var value = scope.get(this.exp); + if (this.initialValue && typeof value === "undefined") { + value = this.initialValue; + scope.setEval(this.exp, value); + } + if (this.lastValue != value) { + this.lastChecked = input.checked = this.inputValue == (''+value); + this.lastValue = value; + } + } +}; + +/////////////////////// +//ElementController +/////////////////////// +function BindUpdater(view, exp) { + this.view = view; + this.exp = Binder.parseBindings(exp); + this.hasError = false; +}; + +BindUpdater.toText = function(obj) { + var e = escapeHtml; + switch(typeof obj) { + case "string": + case "boolean": + case "number": + return e(obj); + case "function": + return BindUpdater.toText(obj()); + case "object": + if (isNode(obj)) { + return outerHTML(obj); + } else if (obj instanceof angular.filter.Meta) { + switch(typeof obj.html) { + case "string": + case "number": + return obj.html; + case "function": + return obj.html(); + case "object": + if (isNode(obj.html)) + return outerHTML(obj.html); + default: + break; + } + switch(typeof obj.text) { + case "string": + case "number": + return e(obj.text); + case "function": + return e(obj.text()); + default: + break; + } + } + if (obj === null) + return ""; + return e(toJson(obj, true)); + default: + return ""; + } +}; + +BindUpdater.prototype = { + updateModel: noop, + updateView: function(scope) { + var html = []; + var parts = this.exp; + var length = parts.length; + for(var i=0; i iteratorCounter; --r) { + this.children.pop().element.remove(); + } + // Special case for option in select + if (child && child.element[0].nodeName === "OPTION") { + var select = jQuery(child.element[0].parentNode); + var cntl = select.data('controller'); + if (cntl) { + cntl.lastValue = undefined; + cntl.updateView(scope); + } + } + }); + } +}; + +////////////////////////////////// +// PopUp +////////////////////////////////// + +function PopUp(doc) { + this.doc = doc; +}; + +PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; + +PopUp.onOver = function(e) { + PopUp.onOut(); + var jNode = jQuery(this); + jNode.bind(PopUp.OUT_EVENT, PopUp.onOut); + var position = jNode.position(); + var de = document.documentElement; + var w = self.innerWidth || (de && de.clientWidth) || document.body.clientWidth; + var hasArea = w - position.left; + var width = 300; + var title = jNode.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."; + var msg = jNode.attr("ng-error"); + + var x; + var arrowPos = hasArea>(width+75) ? "left" : "right"; + var tip = jQuery( + "
    " + + "
    " + + "
    "+title+"
    " + + "
    "+msg+"
    " + + "
    "); + jQuery("body").append(tip); + if(arrowPos === 'left'){ + x = position.left + this.offsetWidth + 11; + }else{ + x = position.left - (width + 15); + tip.find('.ng-arrow-right').css({left:width+1}); + } + + tip.css({left: x+"px", top: (position.top - 3)+"px"}); + return true; +}; + +PopUp.onOut = function() { + jQuery('#ng-callout'). + unbind(PopUp.OUT_EVENT, PopUp.onOut). + remove(); + return true; +}; + +PopUp.prototype = { + bind: function () { + var self = this; + this.doc.find('.ng-validation-error,.ng-exception'). + live("mouseover", PopUp.onOver); + } +}; + +////////////////////////////////// +// Status +////////////////////////////////// + +function NullStatus(body) { +}; + +NullStatus.prototype = { + beginRequest:function(){}, + endRequest:function(){} +}; + +function Status(body) { + this.requestCount = 0; + this.body = body; +}; + +Status.DOM ='
    loading....
    '; + +Status.prototype = { + beginRequest: function () { + if (this.requestCount === 0) { + (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show(); + } + this.requestCount++; + }, + + endRequest: function () { + this.requestCount--; + if (this.requestCount === 0) { + this.loader.hide("fold"); + } + } +}; diff --git a/src/directives.js b/src/directives.js new file mode 100644 index 00000000..cabf0c23 --- /dev/null +++ b/src/directives.js @@ -0,0 +1,261 @@ +angularDirective("ng-init", function(expression){ + return function(element){ + this.$tryEval(expression, element); + }; +}); + +angularDirective("ng-controller", function(expression){ + return function(element){ + var controller = getter(window, expression, true) || getter(this, expression, true); + if (!controller) + throw "Can not find '"+expression+"' controller."; + if (!isFunction(controller)) + throw "Reference '"+expression+"' is not a class."; + this.$become(controller); + (this.init || noop)(); + }; +}); + +angularDirective("ng-eval", function(expression){ + return function(element){ + this.$onEval(expression, element); + }; +}); + +angularDirective("ng-bind", function(expression){ + return function(element) { + var lastValue = noop, lastError = noop; + this.$onEval(function() { + var error, + value = this.$tryEval(expression, function(e){ + error = toJson(e); + }), + isHtml, + isDomElement; + if (lastValue === value && lastError == error) return; + isHtml = value instanceof HTML, + isDomElement = isElement(value); + if (!isHtml && !isDomElement && isObject(value)) { + value = toJson(value); + } + if (value != lastValue || error != lastError) { + lastValue = value; + lastError = error; + elementError(element, NG_EXCEPTION, error); + if (error) value = error; + if (isHtml) { + element.html(value.html); + } else if (isDomElement) { + element.html(''); + element.append(value); + } else { + element.text(value); + } + } + }, element); + }; +}); + +var bindTemplateCache = {}; +function compileBindTemplate(template){ + var fn = bindTemplateCache[template]; + if (!fn) { + var bindings = []; + foreach(parseBindings(template), function(text){ + var exp = binding(text); + bindings.push(exp ? function(element){ + var error, value = this.$tryEval(exp, function(e){ + error = toJson(e); + }); + elementError(element, NG_EXCEPTION, error); + return error ? error : value; + } : function() { + return text; + }); + }); + bindTemplateCache[template] = fn = function(element){ + var parts = [], self = this; + for ( var i = 0; i < bindings.length; i++) { + var value = bindings[i].call(self, element); + if (isElement(value)) + value = ''; + else if (isObject(value)) + value = toJson(value, true); + parts.push(value); + }; + return parts.join(''); + }; + } + return fn; +} + +angularDirective("ng-bind-template", function(expression){ + var templateFn = compileBindTemplate(expression); + return function(element) { + var lastValue; + this.$onEval(function() { + var value = templateFn.call(this, element); + if (value != lastValue) { + element.text(value); + lastValue = value; + } + }, element); + }; +}); + +var REMOVE_ATTRIBUTES = { + 'disabled':'disabled', + 'readonly':'readOnly', + 'checked':'checked' +}; +angularDirective("ng-bind-attr", function(expression){ + return function(element){ + var lastValue = {}; + this.$onEval(function(){ + var values = this.$eval(expression); + for(var key in values) { + var value = compileBindTemplate(values[key]).call(this, element), + specialName = REMOVE_ATTRIBUTES[lowercase(key)]; + if (lastValue[key] !== value) { + lastValue[key] = value; + if (specialName) { + if (element[specialName] = toBoolean(value)) { + element.attr(specialName, value); + } else { + element.removeAttr(key); + } + (element.data('$validate')||noop)(); + } else { + element.attr(key, value); + } + } + }; + }, element); + }; +}); + +angularWidget("@ng-non-bindable", noop); + +angularWidget("@ng-repeat", function(expression, element){ + element.removeAttr('ng-repeat'); + element.replaceWith(this.comment("ng-repeat: " + expression)); + var template = this.compile(element); + return function(reference){ + var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), + lhs, rhs, valueIdent, keyIdent; + if (! match) { + throw "Expected ng-repeat in form of 'item in collection' but got '" + + expression + "'."; + } + lhs = match[1]; + rhs = match[2]; + match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); + if (!match) { + throw "'item' in 'item in collection' should be identifier or (key, value) but got '" + + keyValue + "'."; + } + valueIdent = match[3] || match[1]; + keyIdent = match[2]; + + if (isUndefined(this.$eval(rhs))) this.$set(rhs, []); + + var children = [], currentScope = this; + this.$onEval(function(){ + var index = 0, childCount = children.length, childScope, lastElement = reference, + collection = this.$tryEval(rhs, reference); + for ( var key in collection) { + if (index < childCount) { + // reuse existing child + childScope = children[index]; + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + } else { + // grow children + childScope = template(element.clone(), createScope(currentScope)); + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + lastElement.after(childScope.$element); + childScope.$index = index; + childScope.$element.attr('ng-repeat-index', index); + childScope.$init(); + children.push(childScope); + } + childScope.$eval(); + lastElement = childScope.$element; + index ++; + }; + // shrink children + while(children.length > index) { + children.pop().$element.remove(); + } + }, reference); + }; +}); + +angularDirective("ng-click", function(expression, element){ + return function(element){ + var self = this; + element.bind('click', function(){ + self.$tryEval(expression, element); + self.$root.$eval(); + return false; + }); + }; +}); + +angularDirective("ng-watch", function(expression, element){ + return function(element){ + var self = this; + new Parser(expression).watch()({ + addListener:function(watch, exp){ + self.$watch(watch, function(){ + return exp(self); + }, element); + } + }); + }; +}); + +function ngClass(selector) { + return function(expression, element){ + var existing = element[0].className + ' '; + return function(element){ + this.$onEval(function(){ + var value = this.$eval(expression); + if (selector(this.$index)) { + if (isArray(value)) value = value.join(' '); + element[0].className = trim(existing + value); + } + }, element); + }; + }; +} + +angularDirective("ng-class", ngClass(function(){return true;})); +angularDirective("ng-class-odd", ngClass(function(i){return i % 2 === 0;})); +angularDirective("ng-class-even", ngClass(function(i){return i % 2 === 1;})); + +angularDirective("ng-show", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css('display', toBoolean(this.$eval(expression)) ? '' : 'none'); + }, element); + }; +}); + +angularDirective("ng-hide", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css('display', toBoolean(this.$eval(expression)) ? 'none' : ''); + }, element); + }; +}); + +angularDirective("ng-style", function(expression, element){ + return function(element){ + this.$onEval(function(){ + element.css(this.$eval(expression)); + }, element); + }; +}); + diff --git a/src/filters.js b/src/filters.js new file mode 100644 index 00000000..a911b935 --- /dev/null +++ b/src/filters.js @@ -0,0 +1,298 @@ +var angularFilterGoogleChartApi; + +foreach({ + 'currency': function(amount){ + this.$element.toggleClass('ng-format-negative', amount < 0); + return '$' + angularFilter['number'].apply(this, [amount, 2]); + }, + + 'number': function(amount, fractionSize){ + if (isNaN(amount) || !isFinite(amount)) { + return ''; + } + fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; + var isNegative = amount < 0; + amount = Math.abs(amount); + var pow = Math.pow(10, fractionSize); + var text = "" + Math.round(amount * pow); + var whole = text.substring(0, text.length - fractionSize); + whole = whole || '0'; + var frc = text.substring(text.length - fractionSize); + text = isNegative ? '-' : ''; + for (var i = 0; i < whole.length; i++) { + if ((whole.length - i)%3 === 0 && i !== 0) { + text += ','; + } + text += whole.charAt(i); + } + if (fractionSize > 0) { + for (var j = frc.length; j < fractionSize; j++) { + frc += '0'; + } + text += '.' + frc.substring(0, fractionSize); + } + return text; + }, + + 'date': function(amount) { + }, + + 'json': function(object) { + this.$element.addClass("ng-monospace"); + return toJson(object, true); + }, + + 'trackPackage': (function(){ + var MATCHERS = [ + { name: "UPS", + url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=", + regexp: [ + /^1Z[0-9A-Z]{16}$/i]}, + { name: "FedEx", + url: "http://www.fedex.com/Tracking?tracknumbers=", + regexp: [ + /^96\d{10}?$/i, + /^96\d{17}?$/i, + /^96\d{20}?$/i, + /^\d{15}$/i, + /^\d{12}$/i]}, + { name: "USPS", + url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=", + regexp: [ + /^(91\d{20})$/i, + /^(91\d{18})$/i]}]; + return function(trackingNo, noMatch) { + trackingNo = trim(trackingNo); + var tNo = trackingNo.replace(/ /g, ''); + var returnValue; + foreach(MATCHERS, function(carrier){ + foreach(carrier.regexp, function(regexp){ + if (!returnValue && regexp.test(tNo)) { + var text = carrier.name + ": " + trackingNo; + var url = carrier.url + trackingNo; + returnValue = jqLite(''); + returnValue.text(text); + returnValue.attr('href', url); + } + }); + }); + if (returnValue) + return returnValue; + else if (trackingNo) + return noMatch || trackingNo + " is not recognized"; + else + return null; + };})(), + + 'link': function(obj, title) { + if (obj) { + var text = title || obj.text || obj; + var url = obj.url || obj; + if (url) { + if (angular.validator.email(url) === null) { + url = "mailto:" + url; + } + var a = jqLite(''); + a.attr('href', url); + a.text(text); + return a; + } + } + return obj; + }, + + + 'bytes': (function(){ + var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + return function(size) { + if(size === null) return ""; + + var suffix = 0; + while (size > 1000) { + size = size / 1024; + suffix++; + } + var txt = "" + size; + var dot = txt.indexOf('.'); + if (dot > -1 && dot + 2 < txt.length) { + txt = txt.substring(0, dot + 2); + } + return txt + " " + SUFFIX[suffix]; + }; + })(), + + 'image': function(obj, width, height) { + if (obj && obj.url) { + var style = "", img = jqLite(''); + if (width) { + img.css('max-width', width + 'px'); + img.css('max-height', (height || width) + 'px'); + } + img.attr('src', obj.url); + return img; + } + return null; + }, + + 'lowercase': lowercase, + + 'uppercase': uppercase, + + 'linecount': function (obj) { + if (isString(obj)) { + if (obj==='') return 1; + return obj.split(/\n|\f/).length; + } + return 1; + }, + + 'if': function (result, expression) { + return expression ? result : undefined; + }, + + 'unless': function (result, expression) { + return expression ? undefined : result; + }, + + 'googleChartApi': extend( + function(type, data, width, height) { + data = data || {}; + var chart = { + 'cht':type, + 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), + 'chtt':angularFilterGoogleChartApi['title'](data), + 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), + 'chd':angularFilterGoogleChartApi['values'](data), + 'chf':'bg,s,FFFFFF00' + }; + if (_.isArray(data['xLabels'])) { + chart['chxt']='x'; + chart['chxl']='0:|' + data.xLabels.join('|'); + } + return angularFilterGoogleChartApi['encode'](chart, width, height); + }, + { + 'values': function(data){ + var seriesValues = []; + foreach(data['series']||[], function(serie){ + var values = []; + foreach(serie['values']||[], function(value){ + values.push(value); + }); + seriesValues.push(values.join(',')); + }); + var values = seriesValues.join('|'); + return values === "" ? null : "t:" + values; + }, + + 'title': function(data){ + var titles = []; + var title = data['title'] || []; + foreach(_.isArray(title)?title:[title], function(text){ + titles.push(encodeURIComponent(text)); + }); + return titles.join('|'); + }, + + 'collect': function(data, key){ + var outterValues = []; + var count = 0; + foreach(data['series']||[], function(serie){ + var innerValues = []; + var value = serie[key] || []; + foreach(_.isArray(value)?value:[value], function(color){ + innerValues.push(encodeURIComponent(color)); + count++; + }); + outterValues.push(innerValues.join('|')); + }); + return count?outterValues.join(','):null; + }, + + 'encode': function(params, width, height) { + width = width || 200; + height = height || width; + var url = "http://chart.apis.google.com/chart?", + urlParam = [], + img = jqLite(''); + params['chs'] = width + "x" + height; + foreach(params, function(value, key){ + if (value) { + urlParam.push(key + "=" + value); + } + }); + urlParam.sort(); + url += urlParam.join("&"); + img.attr('src', url); + img.css({width: width + 'px', height: height + 'px'}); + return img; + } + } + ), + + + 'qrcode': function(value, width, height) { + return angularFilterGoogleChartApi['encode']({ + 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); + }, + 'chart': { + 'pie':function(data, width, height) { + return angularFilterGoogleChartApi('p', data, width, height); + }, + 'pie3d':function(data, width, height) { + return angularFilterGoogleChartApi('p3', data, width, height); + }, + 'pieConcentric':function(data, width, height) { + return angularFilterGoogleChartApi('pc', data, width, height); + }, + 'barHorizontalStacked':function(data, width, height) { + return angularFilterGoogleChartApi('bhs', data, width, height); + }, + 'barHorizontalGrouped':function(data, width, height) { + return angularFilterGoogleChartApi('bhg', data, width, height); + }, + 'barVerticalStacked':function(data, width, height) { + return angularFilterGoogleChartApi('bvs', data, width, height); + }, + 'barVerticalGrouped':function(data, width, height) { + return angularFilterGoogleChartApi('bvg', data, width, height); + }, + 'line':function(data, width, height) { + return angularFilterGoogleChartApi('lc', data, width, height); + }, + 'sparkline':function(data, width, height) { + return angularFilterGoogleChartApi('ls', data, width, height); + }, + 'scatter':function(data, width, height) { + return angularFilterGoogleChartApi('s', data, width, height); + } + }, + + 'html': function(html){ + return new HTML(html); + }, + + 'linky': function(text){ + if (!text) return text; + function regExpEscape(text) { + return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); + } + var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; + var match; + var raw = text; + var html = []; + while (match=raw.match(URL)) { + var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); + var i = raw.indexOf(url); + html.push(escapeHtml(raw.substr(0, i))); + html.push(''); + html.push(url); + html.push(''); + raw = raw.substring(i + url.length); + } + html.push(escapeHtml(raw)); + return new HTML(html.join('')); + } +}, function(v,k){angularFilter[k] = v;}); + +angularFilterGoogleChartApi = angularFilter['googleChartApi']; diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 00000000..40462cf3 --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,32 @@ +function formatter(format, parse) {return {'format':format, 'parse':parse || format};} +function toString(obj) {return (isDefined(obj) && obj !== null) ? "" + obj : obj;} + +var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/; + +extend(angularFormatter, { + 'noop':formatter(identity, identity), + 'boolean':formatter(toString, toBoolean), + 'number':formatter(toString, + function(obj){ + if (isString(obj) && NUMBER.exec(obj)) { + return obj ? 1*obj : null; + } + throw "Not a number"; + }), + + 'list':formatter( + function(obj) { return obj ? obj.join(", ") : obj; }, + function(value) { + var list = []; + foreach((value || '').split(','), function(item){ + item = trim(item); + if (item) list.push(item); + }); + return list; + } + ), + + 'trim':formatter( + function(obj) { return obj ? trim("" + obj) : ""; } + ) +}); diff --git a/src/jqLite.js b/src/jqLite.js new file mode 100644 index 00000000..68172fd8 --- /dev/null +++ b/src/jqLite.js @@ -0,0 +1,248 @@ +////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}; +var jqName = 'ng-' + new Date().getTime(); +var jqId = 1; +function jqNextId() { return (jqId++); } + +var addEventListener = window.document.attachEvent ? + function(element, type, fn) { + element.attachEvent('on' + type, fn); + } : function(element, type, fn) { + element.addEventListener(type, fn, false); + }; + +var removeEventListener = window.document.detachEvent ? + function(element, type, fn) { + element.detachEvent('on' + type, fn); + } : function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; + +function jqClearData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + foreach(cache.bind || {}, function(fn, type){ + removeEventListener(element, type, fn); + }); + delete jqCache[cacheId]; + if (msie) + element[jqName] = ''; // ie does not allow deletion of attributes on elements. + else + delete element[jqName]; + } +} + +function JQLite(element) { + if (isElement(element)) { + this[0] = element; + this.length = 1; + } else if (isDefined(element.length) && element.item) { + for(var i=0; i < element.length; i++) { + this[i] = element[i]; + } + this.length = element.length; + } +} + +JQLite.prototype = { + data: function(key, value) { + var element = this[0], + cacheId = element[jqName], + cache = jqCache[cacheId || -1]; + if (isDefined(value)) { + if (!cache) { + element[jqName] = cacheId = jqNextId(); + cache = jqCache[cacheId] = {}; + } + cache[key] = value; + } else { + return cache ? cache[key] : null; + } + }, + + removeData: function(){ + jqClearData(this[0]); + }, + + dealoc: function(){ + (function dealoc(element){ + jqClearData(element); + for ( var i = 0, children = element.childNodes; i < children.length; i++) { + dealoc(children[i]); + } + })(this[0]); + }, + + bind: function(type, fn){ + var self = this, + element = self[0], + bind = self.data('bind'), + eventHandler; + if (!bind) this.data('bind', bind = {}); + foreach(type.split(' '), function(type){ + eventHandler = bind[type]; + if (!eventHandler) { + bind[type] = eventHandler = function(event) { + var bubbleEvent = false; + foreach(eventHandler.fns, function(fn){ + bubbleEvent = bubbleEvent || fn.call(self, event); + }); + if (!bubbleEvent) { + if (msie) { + event.returnValue = false; + event.cancelBubble = true; + } else { + event.preventDefault(); + event.stopPropagation(); + } + } + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }); + }, + + trigger: function(type) { + var evnt = document.createEvent('MouseEvent'); + evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + this[0].dispatchEvent(evnt); + }, + + replaceWith: function(replaceNode) { + this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); + }, + + children: function() { + return new JQLite(this[0].childNodes); + }, + + append: function(node) { + var self = this[0]; + node = jqLite(node); + foreach(node, function(child){ + self.appendChild(child); + }); + }, + + remove: function() { + this.dealoc(); + var parentNode = this[0].parentNode; + if (parentNode) parentNode.removeChild(this[0]); + }, + + removeAttr: function(name) { + this[0].removeAttribute(name); + }, + + after: function(element) { + this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); + }, + + hasClass: function(selector) { + var className = " " + selector + " "; + if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { + return true; + } + return false; + }, + + removeClass: function(selector) { + this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", "")); + }, + + toggleClass: function(selector, condition) { + var self = this; + (condition ? self.addClass : self.removeClass).call(self, selector); + }, + + addClass: function( selector ) { + if (!this.hasClass(selector)) { + this[0].className = trim(this[0].className + ' ' + selector); + } + }, + + css: function(name, value) { + var style = this[0].style; + if (isString(name)) { + if (isDefined(value)) { + style[name] = value; + } else { + return style[name]; + } + } else { + extend(style, name); + } + }, + + attr: function(name, value){ + var e = this[0]; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); + } else { + var attributes = e.attributes, + item = attributes ? attributes.getNamedItem(name) : undefined; + return item && item.specified ? item.value : undefined; + } + }, + + text: function(value) { + if (isDefined(value)) { + this[0].textContent = value; + } + return this[0].textContent; + }, + + val: function(value) { + if (isDefined(value)) { + this[0].value = value; + } + return this[0].value; + }, + + html: function(value) { + if (isDefined(value)) { + var i = 0, childNodes = this[0].childNodes; + for ( ; i < childNodes.length; i++) { + jqLite(childNodes[i]).dealoc(); + } + this[0].innerHTML = value; + } + return this[0].innerHTML; + }, + + parent: function() { + return jqLite(this[0].parentNode); + }, + + clone: function() { return jqLite(this[0].cloneNode(true)); } +}; + +if (msie) { + extend(JQLite.prototype, { + text: function(value) { + var e = this[0]; + // NodeType == 3 is text node + if (e.nodeType == 3) { + if (isDefined(value)) e.nodeValue = value; + return e.nodeValue; + } else { + if (isDefined(value)) e.innerText = value; + return e.innerText; + } + }, + + trigger: function(type) { + this[0].fireEvent('on' + type); + } + }); +} diff --git a/src/markups.js b/src/markups.js new file mode 100644 index 00000000..74b293b8 --- /dev/null +++ b/src/markups.js @@ -0,0 +1,85 @@ +function parseBindings(string) { + var results = []; + var lastIndex = 0; + var index; + while((index = string.indexOf('{{', lastIndex)) > -1) { + if (lastIndex < index) + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + + index = string.indexOf('}}', index); + index = index < 0 ? string.length : index + 2; + + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + } + if (lastIndex != string.length) + results.push(string.substr(lastIndex, string.length - lastIndex)); + return results.length === 0 ? [ string ] : results; +} + +function binding(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +} + +function hasBindings(bindings) { + return bindings.length > 1 || binding(bindings[0]) !== null; +} + +angularTextMarkup('{{}}', function(text, textNode, parentElement) { + var bindings = parseBindings(text), + self = this; + if (hasBindings(bindings)) { + if (isLeafNode(parentElement[0])) { + parentElement.attr('ng-bind-template', text); + } else { + var cursor = textNode, newElement; + foreach(parseBindings(text), function(text){ + var exp = binding(text); + if (exp) { + newElement = self.element('span'); + newElement.attr('ng-bind', exp); + } else { + newElement = self.text(text); + } + if (msie && text.charAt(0) == ' ') { + newElement = jqLite(' '); + var nbsp = newElement.html(); + newElement.text(text.substr(1)); + newElement.html(nbsp + newElement.html()); + } + cursor.after(newElement); + cursor = newElement; + }); + } + textNode.remove(); + } +}); + +// TODO: this should be widget not a markup +angularTextMarkup('OPTION', function(text, textNode, parentElement){ + if (nodeName(parentElement) == "OPTION") { + var select = document.createElement('select'); + select.insertBefore(parentElement[0].cloneNode(true), null); + if (!select.innerHTML.match(/.*<\/\s*option\s*>/gi)) { + parentElement.attr('value', text); + } + } +}); + +var NG_BIND_ATTR = 'ng-bind-attr'; +angularAttrMarkup('{{}}', function(value, name, element){ + if (name.substr(0, 3) != 'ng-') { + if (msie && name == 'src') + value = decodeURI(value); + var bindings = parseBindings(value), + bindAttr; + if (hasBindings(bindings)) { + element.removeAttr(name); + bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}"); + bindAttr[name] = value; + element.attr(NG_BIND_ATTR, toJson(bindAttr)); + } + } +}); diff --git a/src/moveToAngularCom/ControlBar.js b/src/moveToAngularCom/ControlBar.js new file mode 100644 index 00000000..685beeb2 --- /dev/null +++ b/src/moveToAngularCom/ControlBar.js @@ -0,0 +1,72 @@ +function ControlBar(document, serverUrl, database) { + this._document = document; + this.serverUrl = serverUrl; + this.database = database; + this._window = window; + this.callbacks = []; +}; + +ControlBar.HTML = + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    '; + + +ControlBar.FORBIDEN = + '
    ' + + 'Sorry, you do not have permission for this!'+ + '
    '; + +ControlBar.prototype = { + bind: function () { + }, + + login: function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/new.mini?database="+encodeURIComponent(this.database)+"&return_url=" + encodeURIComponent(this.urlWithoutAnchor())); + } + }, + + logout: function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/do_destroy.mini"); + } + }, + + urlWithoutAnchor: function (path) { + return this._window['location']['href'].split("#")[0]; + }, + + doTemplate: function (path) { + var self = this; + var id = new Date().getTime(); + var url = this.urlWithoutAnchor() + "#$iframe_notify=" + id; + var iframeHeight = 330; + var loginView = jQuery('
    ' + + '
    '); + 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); + } + }; + callback(); + }, + + addStep: function(name, step) { + this.currentSpec.steps.push({name:name, fn:step}); + }, + + execute: function(name, callback) { + var spec = this.specs[name], + self = this, + 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, + testFrame: this.testFrame, + testWindow: this.testWindow + }, angularService, {}); + this.self = specThis; + var stepLogger = this.logger('spec', name); + spec.nextStepIndex = 0; + function done() { + result.finished = true; + stepLogger.close(); + self.self = null; + (callback||noop).call(specThis); + } + function next(){ + var step = spec.steps[spec.nextStepIndex]; + (result.log || {close:noop}).close(); + result.log = null; + if (step) { + spec.nextStepIndex ++; + result.log = stepLogger('step', step.name); + try { + step.fn.call(specThis, next); + } catch (e) { + console.error(e); + result.fail(e); + done(); + } + } else { + result.passed = !result.failed; + done(); + } + }; + next(); + return specThis; + } +}; \ No newline at end of file diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix new file mode 100644 index 00000000..5b44e17c --- /dev/null +++ b/src/scenario/angular.prefix @@ -0,0 +1,30 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window, document, previousOnLoad){ + window.angular = { + scenario: { + dsl: window + } + }; + diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix new file mode 100644 index 00000000..fc861cbf --- /dev/null +++ b/src/scenario/angular.suffix @@ -0,0 +1,11 @@ + + var $scenarioRunner = new angular.scenario.Runner(window, jQuery); + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + $scenarioRunner.run(jQuery(window.document.body)); + }; + +})(window, document, window.onload); diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js new file mode 100644 index 00000000..694d0e97 --- /dev/null +++ b/src/scenario/bootstrap.js @@ -0,0 +1,44 @@ +(function(onLoadDelegate){ + 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.angular = { + scenario: { + dsl: window + } + }; + + window.onload = function(){ + _.defer(function(){ + $scenarioRunner.run(jQuery(window.document.body)); + }); + (onLoadDelegate||function(){})(); + }; + addCSS("../../css/angular-scenario.css"); + addScript("../../lib/underscore/underscore.js"); + addScript("../../lib/jquery/jquery-1.4.2.js"); + addScript("Runner.js"); + addScript("../Angular.js"); + addScript("../JSON.js"); + addScript("DSL.js"); + document.write(''); +})(window.onload); + diff --git a/src/services.js b/src/services.js new file mode 100644 index 00000000..64f2ea4f --- /dev/null +++ b/src/services.js @@ -0,0 +1,361 @@ +angularService("$window", bind(window, identity, window)); +angularService("$document", function(window){ + return jqLite(window.document); +}, {inject:['$window']}); + +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/; +var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/; +var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; +angularService("$location", function(browser){ + var scope = this, location = {parse:parseUrl, toString:toString}; + var lastHash, lastUrl; + function parseUrl(url){ + if (isDefined(url)) { + var match = URL_MATCH.exec(url); + if (match) { + location.href = url; + location.protocol = match[1]; + location.host = match[3] || ''; + location.port = match[5] || DEFAULT_PORTS[location.href] || null; + location.path = match[6]; + location.search = parseKeyValue(match[8]); + location.hash = match[9] || ''; + if (location.hash) + location.hash = location.hash.substr(1); + parseHash(location.hash); + } + } + } + function parseHash(hash) { + var match = HASH_MATCH.exec(hash); + location.hashPath = match[1] || ''; + location.hashSearch = parseKeyValue(match[3]); + lastHash = hash; + } + function toString() { + if (lastHash === location.hash) { + var hashKeyValue = toKeyValue(location.hashSearch), + hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''), + url = location.href.split('#')[0] + '#' + (hash ? hash : ''); + if (url !== location.href) parseUrl(url); + return url; + } else { + parseUrl(location.href.split('#')[0] + '#' + location.hash); + return toString(); + } + } + browser.watchUrl(function(url){ + parseUrl(url); + scope.$root.$eval(); + }); + parseUrl(browser.getUrl()); + this.$onEval(PRIORITY_FIRST, function(){ + if (location.hash != lastHash) { + parseHash(location.hash); + } + }); + this.$onEval(PRIORITY_LAST, function(){ + var url = toString(); + if (lastUrl != url) { + browser.setUrl(url); + lastUrl = url; + } + }); + return location; +}, {inject: ['$browser']}); + +angularService("$log", function($window){ + var console = $window.console, + log = console && console.log || noop; + return { + log: log, + warn: console && console.warn || log, + info: console && console.info || log, + error: console && console.error || log + }; +}, {inject:['$window']}); + +angularService("$hover", function(browser) { + var tooltip, self = this, error, width = 300, arrowWidth = 10; + browser.hover(function(element, show){ + if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { + if (!tooltip) { + tooltip = { + callout: jqLite('
      '), + arrow: jqLite('
      '), + title: jqLite('
      '), + content: jqLite('
      ') + }; + tooltip.callout.append(tooltip.arrow); + tooltip.callout.append(tooltip.title); + tooltip.callout.append(tooltip.content); + self.$browser.body.append(tooltip.callout); + } + var docRect = self.$browser.body[0].getBoundingClientRect(), + elementRect = element[0].getBoundingClientRect(), + leftSpace = docRect.right - elementRect.right - arrowWidth; + tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); + tooltip.content.text(error); + if (leftSpace < width) { + tooltip.arrow.addClass('ng-arrow-right'); + tooltip.arrow.css({left: (width + 1)+'px'}); + tooltip.callout.css({ + position: 'fixed', + left: (elementRect.left - arrowWidth - width - 4) + "px", + top: (elementRect.top - 3) + "px", + width: width + "px" + }); + } else { + tooltip.arrow.addClass('ng-arrow-left'); + tooltip.callout.css({ + position: 'fixed', + left: (elementRect.right + arrowWidth) + "px", + top: (elementRect.top - 3) + "px", + width: width + "px" + }); + } + } else if (tooltip) { + tooltip.callout.remove(); + tooltip = null; + } + }); +}, {inject:['$browser']}); + +angularService("$invalidWidgets", function(){ + var invalidWidgets = []; + invalidWidgets.markValid = function(element){ + var index = indexOf(invalidWidgets, element); + if (index != -1) + invalidWidgets.splice(index, 1); + }; + invalidWidgets.markInvalid = function(element){ + var index = indexOf(invalidWidgets, element); + if (index === -1) + invalidWidgets.push(element); + }; + invalidWidgets.visible = function() { + var count = 0; + foreach(invalidWidgets, function(widget){ + count = count + (isVisible(widget) ? 1 : 0); + }); + return count; + }; + invalidWidgets.clearOrphans = function() { + for(var i = 0; i < invalidWidgets.length;) { + var widget = invalidWidgets[i]; + if (isOrphan(widget[0])) { + invalidWidgets.splice(i, 1); + } else { + i++; + } + } + }; + function isOrphan(widget) { + if (widget == window.document) return false; + var parent = widget.parentNode; + return !parent || isOrphan(parent); + } + return invalidWidgets; +}); + +function switchRouteMatcher(on, when, dstName) { + var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', + params = [], + dst = {}; + foreach(when.split(/\W/), function(param){ + if (param) { + var paramRegExp = new RegExp(":" + param + "([\\W])"); + if (regex.match(paramRegExp)) { + regex = regex.replace(paramRegExp, "([^\/]*)$1"); + params.push(param); + } + } + }); + var match = on.match(new RegExp(regex)); + if (match) { + foreach(params, function(name, index){ + dst[name] = match[index + 1]; + }); + if (dstName) this.$set(dstName, dst); + } + return match ? dst : null; +} + +angularService('$route', function(location, params){ + var routes = {}, + onChange = [], + matcher = switchRouteMatcher, + parentScope = this, + dirty = 0, + $route = { + routes: routes, + onChange: bind(onChange, onChange.push), + when:function (path, params){ + if (angular.isUndefined(path)) return routes; + var route = routes[path]; + if (!route) route = routes[path] = {}; + if (params) angular.extend(route, params); + dirty++; + return route; + } + }; + function updateRoute(){ + var childScope; + $route.current = null; + angular.foreach(routes, function(routeParams, route) { + if (!childScope) { + var pathParams = matcher(location.hashPath, route); + if (pathParams) { + childScope = angular.scope(parentScope); + $route.current = angular.extend({}, routeParams, { + scope: childScope, + params: angular.extend({}, location.hashSearch, pathParams) + }); + } + } + }); + angular.foreach(onChange, parentScope.$tryEval); + if (childScope) { + childScope.$become($route.current.controller); + parentScope.$tryEval(childScope.init); + } + } + this.$watch(function(){return dirty + location.hash;}, updateRoute); + return $route; +}, {inject: ['$location']}); + +angularService('$xhr', function($browser, $error, $log){ + var self = this; + return function(method, url, post, callback){ + if (isFunction(post)) { + callback = post; + post = null; + } + if (post && isObject(post)) { + post = toJson(post); + } + $browser.xhr(method, url, post, function(code, response){ + try { + if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { + response = fromJson(response); + } + if (code == 200) { + callback(code, response); + } else { + $error( + {method: method, url:url, data:post, callback:callback}, + {status: code, body:response}); + } + } catch (e) { + $log.error(e); + } finally { + self.$eval(); + } + }); + }; +}, {inject:['$browser', '$xhr.error', '$log']}); + +angularService('$xhr.error', function($log){ + return function(request, response){ + $log.error('ERROR: XHR: ' + request.url, request, response); + }; +}, {inject:['$log']}); + +angularService('$xhr.bulk', function($xhr, $error, $log){ + var requests = [], + scope = this; + function bulkXHR(method, url, post, callback) { + if (isFunction(post)) { + callback = post; + post = null; + } + var currentQueue; + foreach(bulkXHR.urls, function(queue){ + if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { + currentQueue = queue; + } + }); + if (currentQueue) { + if (!currentQueue.requests) currentQueue.requests = []; + currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); + } else { + $xhr(method, url, post, callback); + } + } + bulkXHR.urls = {}; + bulkXHR.flush = function(callback){ + foreach(bulkXHR.urls, function(queue, url){ + var currentRequests = queue.requests; + if (currentRequests && currentRequests.length) { + queue.requests = []; + queue.callbacks = []; + $xhr('POST', url, {requests:currentRequests}, function(code, response){ + foreach(response, function(response, i){ + try { + if (response.status == 200) { + (currentRequests[i].callback || noop)(response.status, response.response); + } else { + $error(currentRequests[i], response); + } + } catch(e) { + $log.error(e); + } + }); + (callback || noop)(); + }); + scope.$eval(); + } + }); + }; + this.$onEval(PRIORITY_LAST, bulkXHR.flush); + return bulkXHR; +}, {inject:['$xhr', '$xhr.error', '$log']}); + +angularService('$xhr.cache', function($xhr){ + var inflight = {}, self = this;; + function cache(method, url, post, callback, cacheThenRetrieve){ + if (isFunction(post)) { + callback = post; + post = null; + } + if (method == 'GET') { + var data; + if (data = cache.data[url]) { + callback(200, copy(data.value)); + if (!cacheThenRetrieve) + return; + } + + if (data = inflight[url]) { + data.callbacks.push(callback); + } else { + inflight[url] = {callbacks: [callback]}; + cache.delegate(method, url, post, function(status, response){ + if (status == 200) + cache.data[url] = { value: response }; + var callbacks = inflight[url].callbacks; + delete inflight[url]; + foreach(callbacks, function(callback){ + try { + (callback||noop)(status, copy(response)); + } catch(e) { + self.$log.error(e); + } + }); + }); + } + + } else { + cache.data = {}; + cache.delegate(method, url, post, callback); + } + } + cache.data = {}; + cache.delegate = $xhr; + return cache; +}, {inject:['$xhr.bulk']}); + +angularService('$resource', function($xhr){ + var resource = new ResourceFactory($xhr); + return bind(resource, resource.route); +}, {inject: ['$xhr.cache']}); diff --git a/src/validators.js b/src/validators.js new file mode 100644 index 00000000..5c7fc952 --- /dev/null +++ b/src/validators.js @@ -0,0 +1,132 @@ +foreach({ + 'noop': function() { return null; }, + + 'regexp': function(value, regexp, msg) { + if (!value.match(regexp)) { + return msg || + "Value does not match expected format " + regexp + "."; + } else { + return null; + } + }, + + 'number': function(value, min, max) { + var num = 1 * value; + if (num == value) { + if (typeof min != 'undefined' && num < min) { + return "Value can not be less than " + min + "."; + } + if (typeof min != 'undefined' && num > max) { + return "Value can not be greater than " + max + "."; + } + return null; + } else { + return "Not a number"; + } + }, + + 'integer': function(value, min, max) { + var numberError = angularValidator['number'](value, min, max); + if (numberError) return numberError; + if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { + return "Not a whole number"; + } + return null; + }, + + 'date': function(value, min, max) { + if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { + return null; + } + return "Value is not a date. (Expecting format: 12/31/2009)."; + }, + + 'ssn': function(value) { + if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { + return null; + } + return "SSN needs to be in 999-99-9999 format."; + }, + + 'email': function(value) { + if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { + return null; + } + return "Email needs to be in username@host.com format."; + }, + + 'phone': function(value) { + if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { + return null; + } + if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) { + return null; + } + return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; + }, + + 'url': function(value) { + if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { + return null; + } + return "URL needs to be in http://server[:port]/path format."; + }, + + 'json': function(value) { + try { + fromJson(value); + return null; + } catch (e) { + return e.toString(); + } + }, + + /* + * cache is attached to the element + * cache: { + * inputs : { + * 'user input': { + * response: server response, + * error: validation error + * }, + * current: 'current input' + * } + * + */ + 'asynchronous': function(input, asynchronousFn, updateFn) { + if (!input) return; + var scope = this; + var element = scope.$element; + var cache = element.data('$asyncValidator'); + if (!cache) { + element.data('$asyncValidator', cache = {inputs:{}}); + } + + cache.current = input; + + var inputState = cache.inputs[input]; + if (!inputState) { + cache.inputs[input] = inputState = { inFlight: true }; + scope.$invalidWidgets.markInvalid(scope.$element); + element.addClass('ng-input-indicator-wait'); + asynchronousFn(input, function(error, data) { + inputState.response = data; + inputState.error = error; + inputState.inFlight = false; + if (cache.current == input) { + element.removeClass('ng-input-indicator-wait'); + scope.$invalidWidgets.markValid(element); + } + element.data('$validate')(); + scope.$root.$eval(); + }); + } else if (inputState.inFlight) { + // request in flight, mark widget invalid, but don't show it to user + scope.$invalidWidgets.markInvalid(scope.$element); + } else { + (updateFn||noop)(inputState.response); + } + return inputState.error; + } + +}, function(v,k) {angularValidator[k] = v;}); diff --git a/src/widgets.js b/src/widgets.js new file mode 100644 index 00000000..efafa9c5 --- /dev/null +++ b/src/widgets.js @@ -0,0 +1,328 @@ +function modelAccessor(scope, element) { + var expr = element.attr('name'); + if (!expr) throw "Required field 'name' not found."; + return { + get: function() { + return scope.$eval(expr); + }, + set: function(value) { + if (value !== undefined) { + return scope.$tryEval(expr + '=' + toJson(value), element); + } + } + }; +} + +function modelFormattedAccessor(scope, element) { + var accessor = modelAccessor(scope, element), + formatterName = element.attr('ng-format') || NOOP, + formatter = angularFormatter(formatterName); + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + return { + get: function() { + return formatter.format(accessor.get()); + }, + set: function(value) { + return accessor.set(formatter.parse(value)); + } + }; +} + +function compileValidator(expr) { + return new Parser(expr).validator()(); +} + +function valueAccessor(scope, element) { + var validatorName = element.attr('ng-validate') || NOOP, + validator = compileValidator(validatorName), + requiredExpr = element.attr('ng-required'), + formatterName = element.attr('ng-format') || NOOP, + formatter = angularFormatter(formatterName), + format, parse, lastError, required; + invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop}; + if (!validator) throw "Validator named '" + validatorName + "' not found."; + if (!formatter) throw "Formatter named '" + formatterName + "' not found."; + format = formatter.format; + parse = formatter.parse; + if (requiredExpr) { + scope.$watch(requiredExpr, function(newValue) { + required = newValue; + validate(); + }); + } else { + required = requiredExpr === ''; + } + + element.data('$validate', validate); + return { + get: function(){ + if (lastError) + elementError(element, NG_VALIDATION_ERROR, null); + try { + var value = parse(element.val()); + validate(); + return value; + } catch (e) { + lastError = e; + elementError(element, NG_VALIDATION_ERROR, e); + } + }, + set: function(value) { + var oldValue = element.val(), + newValue = format(value); + if (oldValue != newValue) { + element.val(newValue || ''); // needed for ie + } + validate(); + } + }; + + function validate() { + var value = trim(element.val()); + if (element[0].disabled || element[0].readOnly) { + elementError(element, NG_VALIDATION_ERROR, null); + invalidWidgets.markValid(element); + } else { + var error, + validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element}); + error = required && !value ? + 'Required' : + (value ? validator(validateScope, value) : null); + elementError(element, NG_VALIDATION_ERROR, error); + lastError = error; + if (error) { + invalidWidgets.markInvalid(element); + } else { + invalidWidgets.markValid(element); + } + } + } +} + +function checkedAccessor(scope, element) { + var domElement = element[0], elementValue = domElement.value; + return { + get: function(){ + return !!domElement.checked; + }, + set: function(value){ + domElement.checked = toBoolean(value); + } + }; +} + +function radioAccessor(scope, element) { + var domElement = element[0]; + return { + get: function(){ + return domElement.checked ? domElement.value : null; + }, + set: function(value){ + domElement.checked = value == domElement.value; + } + }; +} + +function optionsAccessor(scope, element) { + var options = element[0].options; + return { + get: function(){ + var values = []; + foreach(options, function(option){ + if (option.selected) values.push(option.value); + }); + return values; + }, + set: function(values){ + var keys = {}; + foreach(values, function(value){ keys[value] = true; }); + foreach(options, function(option){ + option.selected = keys[option.value]; + }); + } + }; +} + +function noopAccessor() { return { get: noop, set: noop }; } + +var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue()), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), + INPUT_TYPE = { + 'text': textWidget, + 'textarea': textWidget, + 'hidden': textWidget, + 'password': textWidget, + 'button': buttonWidget, + 'submit': buttonWidget, + 'reset': buttonWidget, + 'image': buttonWidget, + 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), + 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), + 'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(null)), + 'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([])) +// 'file': fileWidget??? + }; + +function initWidgetValue(initValue) { + return function (model, view) { + var value = view.get(); + if (!value && isDefined(initValue)) { + value = copy(initValue); + } + if (isUndefined(model.get()) && isDefined(value)) { + model.set(value); + } + }; +} + +function radioInit(model, view, element) { + var modelValue = model.get(), viewValue = view.get(), input = element[0]; + input.checked = false; + input.name = this.$id + '@' + input.name; + if (isUndefined(modelValue)) { + model.set(modelValue = null); + } + if (modelValue == null && viewValue !== null) { + model.set(viewValue); + } + view.set(modelValue); +} + +function inputWidget(events, modelAccessor, viewAccessor, initFn) { + return function(element) { + var scope = this, + model = modelAccessor(scope, element), + view = viewAccessor(scope, element), + action = element.attr('ng-change') || '', + lastValue; + initFn.call(scope, model, view, element); + this.$eval(element.attr('ng-init')||''); + // Don't register a handler if we are a button (noopAccessor) and there is no action + if (action || modelAccessor !== noopAccessor) { + element.bind(events, function(){ + model.set(view.get()); + lastValue = model.get(); + scope.$tryEval(action, element); + scope.$root.$eval(); + // if we have noop initFn than we are just a button, + // therefore we want to prevent default action + return initFn != noop; + }); + } + view.set(lastValue = model.get()); + scope.$watch(model.get, function(value){ + if (lastValue !== value) { + view.set(lastValue = value); + } + }); + }; +} + +function inputWidgetSelector(element){ + this.directives(true); + return INPUT_TYPE[lowercase(element[0].type)] || noop; +} + +angularWidget('INPUT', inputWidgetSelector); +angularWidget('TEXTAREA', inputWidgetSelector); +angularWidget('BUTTON', inputWidgetSelector); +angularWidget('SELECT', function(element){ + this.descend(true); + return inputWidgetSelector.call(this, element); +}); + + +angularWidget('NG:INCLUDE', function(element){ + var compiler = this, + srcExp = element.attr("src"), + scopeExp = element.attr("scope") || ''; + if (element[0]['ng-compiled']) { + this.descend(true); + this.directives(true); + } else { + element[0]['ng-compiled'] = true; + return function(element){ + var scope = this, childScope; + var changeCounter = 0; + function incrementChange(){ changeCounter++;} + this.$watch(srcExp, incrementChange); + this.$watch(scopeExp, incrementChange); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + this.$watch(function(){return changeCounter;}, function(){ + var src = this.$eval(srcExp), + useScope = this.$eval(scopeExp); + if (src) { + scope.$xhr.cache('GET', src, function(code, response){ + element.html(response); + childScope = useScope || createScope(scope); + compiler.compile(element)(element, childScope); + childScope.$init(); + }); + } + }); + }; + } +}); + +var ngSwitch = angularWidget('NG:SWITCH', function (element){ + var compiler = this, + watchExpr = element.attr("on"), + usingExpr = (element.attr("using") || 'equals'), + usingExprParams = usingExpr.split(":"), + usingFn = ngSwitch[usingExprParams.shift()], + changeExpr = element.attr('change') || '', + cases = []; + if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; + eachNode(element, function(caseElement){ + var when = caseElement.attr('ng-switch-when'); + if (when) { + cases.push({ + when: function(scope, value){ + var args = [value, when]; + foreach(usingExprParams, function(arg){ + args.push(arg); + }); + return usingFn.apply(scope, args); + }, + change: changeExpr, + element: caseElement, + template: compiler.compile(caseElement) + }); + } + }); + + // this needs to be here for IE + foreach(cases, function(_case){ + _case.element.remove(); + }); + + element.html(''); + return function(element){ + var scope = this, childScope; + this.$watch(watchExpr, function(value){ + element.html(''); + childScope = createScope(scope); + foreach(cases, function(switchCase){ + if (switchCase.when(childScope, value)) { + var caseElement = switchCase.element.clone(); + element.append(caseElement); + childScope.$tryEval(switchCase.change, element); + switchCase.template(caseElement, childScope); + if (scope.$invalidWidgets) + scope.$invalidWidgets.clearOrphans(); + childScope.$init(); + } + }); + }); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + }; +}, { + equals: function(on, when) { + return on == when; + }, + route: switchRouteMatcher +}); diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..a1717861 --- /dev/null +++ b/test.sh @@ -0,0 +1,2 @@ +java -jar lib/jstestdriver/JsTestDriver.jar --tests all +# java -jar lib/jstestdriver/JsTestDriver.jar --tests all --config jsTestDriver-jquery.conf diff --git a/test/AngularSpec.js b/test/AngularSpec.js new file mode 100644 index 00000000..de724f03 --- /dev/null +++ b/test/AngularSpec.js @@ -0,0 +1,52 @@ +describe('Angular', function(){ + xit('should fire on updateEvents', function(){ + var onUpdateView = jasmine.createSpy(); + var scope = angular.compile("
      ", { onUpdateView: onUpdateView }); + expect(onUpdateView).wasNotCalled(); + scope.$init(); + scope.$eval(); + expect(onUpdateView).wasCalled(); + }); +}); + +describe("copy", function(){ + it("should return same object", function (){ + var obj = {}; + var arr = []; + assertSame(obj, copy({}, obj)); + assertSame(arr, copy([], arr)); + }); + + it("should copy array", function(){ + var src = [1, {name:"value"}]; + var dst = [{key:"v"}]; + assertSame(dst, copy(src, dst)); + assertEquals([1, {name:"value"}], dst); + assertEquals({name:"value"}, dst[1]); + assertNotSame(src[1], dst[1]); + }); + + it('should copy empty array', function() { + var src = []; + var dst = [{key: "v"}]; + assertEquals([], copy(src, dst)); + assertEquals([], dst); + }); + + it("should copy object", function(){ + var src = {a:{name:"value"}}; + var dst = {b:{key:"v"}}; + assertSame(dst, copy(src, dst)); + assertEquals({a:{name:"value"}}, dst); + assertEquals(src.a, dst.a); + assertNotSame(src.a, dst.a); + }); + + it("should copy primitives", function(){ + expect(copy(null)).toEqual(null); + expect(copy('')).toEqual(''); + expect(copy(123)).toEqual(123); + expect(copy([{key:null}])).toEqual([{key:null}]); + }); + +}); diff --git a/test/ApiTest.js b/test/ApiTest.js new file mode 100644 index 00000000..4035cdbb --- /dev/null +++ b/test/ApiTest.js @@ -0,0 +1,256 @@ +ApiTest = TestCase("ApiTest"); + +ApiTest.prototype.testItShouldReturnTypeOf = function (){ + assertEquals("undefined", angular.Object.typeOf(undefined)); + assertEquals("null", angular.Object.typeOf(null)); + assertEquals("object", angular.Collection.typeOf({})); + assertEquals("array", angular.Array.typeOf([])); + assertEquals("string", angular.Object.typeOf("")); + assertEquals("date", angular.Object.typeOf(new Date())); + assertEquals("element", angular.Object.typeOf(document.body)); + assertEquals("function", angular.Object.typeOf(function(){})); +}; + +ApiTest.prototype.testItShouldReturnSize = function(){ + assertEquals(0, angular.Collection.size({})); + assertEquals(1, angular.Collection.size({a:"b"})); + assertEquals(0, angular.Object.size({})); + assertEquals(1, angular.Array.size([0])); +}; + +ApiTest.prototype.testIncludeIf = function() { + var array = []; + var obj = {}; + + angular.Array.includeIf(array, obj, true); + angular.Array.includeIf(array, obj, true); + assertTrue(includes(array, obj)); + assertEquals(1, array.length); + + angular.Array.includeIf(array, obj, false); + assertFalse(includes(array, obj)); + assertEquals(0, array.length); + + angular.Array.includeIf(array, obj, 'x'); + assertTrue(includes(array, obj)); + assertEquals(1, array.length); + angular.Array.includeIf(array, obj, ''); + assertFalse(includes(array, obj)); + assertEquals(0, array.length); +}; + +ApiTest.prototype.testSum = function(){ + assertEquals(3, angular.Array.sum([{a:"1"}, {a:"2"}], 'a')); +}; + +ApiTest.prototype.testSumContainingNaN = function(){ + assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], 'a')); + assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($){return $.a;})); +}; + +ApiTest.prototype.testInclude = function(){ + assertTrue(angular.Array.include(['a'], 'a')); + assertTrue(angular.Array.include(['a', 'b'], 'a')); + assertTrue(!angular.Array.include(['c'], 'a')); + assertTrue(!angular.Array.include(['c', 'b'], 'a')); +}; + +ApiTest.prototype.testIndex = function(){ + assertEquals(angular.Array.indexOf(['a'], 'a'), 0); + assertEquals(angular.Array.indexOf(['a', 'b'], 'a'), 0); + assertEquals(angular.Array.indexOf(['b', 'a'], 'a'), 1); + assertEquals(angular.Array.indexOf(['b', 'b'],'x'), -1); +}; + +ApiTest.prototype.testRemove = function(){ + var items = ['a', 'b', 'c']; + assertEquals(angular.Array.remove(items, 'q'), 'q'); + assertEquals(items.length, 3); + + assertEquals(angular.Array.remove(items, 'b'), 'b'); + assertEquals(items.length, 2); + + assertEquals(angular.Array.remove(items, 'a'), 'a'); + assertEquals(items.length, 1); + + assertEquals(angular.Array.remove(items, 'c'), 'c'); + assertEquals(items.length, 0); + + assertEquals(angular.Array.remove(items, 'q'), 'q'); + assertEquals(items.length, 0); +}; + +ApiTest.prototype.testFindById = function() { + var items = [{$id:1}, {$id:2}, {$id:3}]; + assertNull(angular.Array.findById(items, 0)); + assertEquals(items[0], angular.Array.findById(items, 1)); + assertEquals(items[1], angular.Array.findById(items, 2)); + assertEquals(items[2], angular.Array.findById(items, 3)); +}; + +ApiTest.prototype.testFilter = function() { + var items = ["MIsKO", {name:"shyam"}, ["adam"], 1234]; + assertEquals(4, angular.Array.filter(items, "").length); + assertEquals(4, angular.Array.filter(items, undefined).length); + + assertEquals(1, angular.Array.filter(items, 'iSk').length); + assertEquals("MIsKO", angular.Array.filter(items, 'isk')[0]); + + assertEquals(1, angular.Array.filter(items, 'yam').length); + assertEquals(items[1], angular.Array.filter(items, 'yam')[0]); + + assertEquals(1, angular.Array.filter(items, 'da').length); + assertEquals(items[2], angular.Array.filter(items, 'da')[0]); + + assertEquals(1, angular.Array.filter(items, '34').length); + assertEquals(1234, angular.Array.filter(items, '34')[0]); + + assertEquals(0, angular.Array.filter(items, "I don't exist").length); +}; + +ApiTest.prototype.testShouldNotFilterOnSystemData = function() { + assertEquals("", "".charAt(0)); // assumption + var items = [{$name:"misko"}]; + assertEquals(0, angular.Array.filter(items, "misko").length); +}; + +ApiTest.prototype.testFilterOnSpecificProperty = function() { + var items = [{ignore:"a", name:"a"}, {ignore:"a", name:"abc"}]; + assertEquals(2, angular.Array.filter(items, {}).length); + + assertEquals(2, angular.Array.filter(items, {name:'a'}).length); + + assertEquals(1, angular.Array.filter(items, {name:'b'}).length); + assertEquals("abc", angular.Array.filter(items, {name:'b'})[0].name); +}; + +ApiTest.prototype.testFilterOnFunction = function() { + var items = [{name:"a"}, {name:"abc", done:true}]; + assertEquals(1, angular.Array.filter(items, function(i){return i.done;}).length); +}; + +ApiTest.prototype.testFilterIsAndFunction = function() { + var items = [{first:"misko", last:"hevery"}, + {first:"adam", last:"abrons"}]; + + assertEquals(2, angular.Array.filter(items, {first:'', last:''}).length); + assertEquals(1, angular.Array.filter(items, {first:'', last:'hevery'}).length); + assertEquals(0, angular.Array.filter(items, {first:'adam', last:'hevery'}).length); + assertEquals(1, angular.Array.filter(items, {first:'misko', last:'hevery'}).length); + assertEquals(items[0], angular.Array.filter(items, {first:'misko', last:'hevery'})[0]); +}; + +ApiTest.prototype.testFilterNot = function() { + var items = ["misko", "adam"]; + + assertEquals(1, angular.Array.filter(items, '!isk').length); + assertEquals(items[1], angular.Array.filter(items, '!isk')[0]); +}; + +ApiTest.prototype.testAdd = function() { + var add = angular.Array.add; + assertJsonEquals([{}, "a"], add(add([]),"a")); +}; + +ApiTest.prototype.testCount = function() { + var array = [{name:'a'},{name:'b'},{name:''}]; + var obj = {}; + + assertEquals(3, angular.Array.count(array)); + assertEquals(2, angular.Array.count(array, 'name')); + assertEquals(1, angular.Array.count(array, 'name=="a"')); +}; + +ApiTest.prototype.testFind = function() { + var array = [{name:'a'},{name:'b'},{name:''}]; + var obj = {}; + + assertEquals(undefined, angular.Array.find(array, 'false')); + assertEquals('default', angular.Array.find(array, 'false', 'default')); + assertEquals('a', angular.Array.find(array, 'name == "a"').name); + assertEquals('', angular.Array.find(array, 'name == ""').name); +}; + +ApiTest.prototype.testItShouldSortArray = function() { + assertEquals([2,15], angular.Array.orderBy([15,2])); + assertEquals(["a","B", "c"], angular.Array.orderBy(["c","B", "a"])); + assertEquals([15,"2"], angular.Array.orderBy([15,"2"])); + assertEquals(["15","2"], angular.Array.orderBy(["15","2"])); + assertJsonEquals([{a:2},{a:15}], angular.Array.orderBy([{a:15},{a:2}], 'a')); + assertJsonEquals([{a:2},{a:15}], angular.Array.orderBy([{a:15},{a:2}], 'a', "F")); +}; + +ApiTest.prototype.testItShouldSortArrayInReverse = function() { + assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', true)); + assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "T")); + assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "reverse")); +}; + +ApiTest.prototype.testItShouldSortArrayByPredicate = function() { + assertJsonEquals([{a:2, b:1},{a:15, b:1}], + angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['a', 'b'])); + assertJsonEquals([{a:2, b:1},{a:15, b:1}], + angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['b', 'a'])); + assertJsonEquals([{a:15, b:1},{a:2, b:1}], + angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['+b', '-a'])); +}; + +ApiTest.prototype.testQuoteString = function(){ + assertEquals(angular.String.quote('a'), '"a"'); + assertEquals(angular.String.quote('\\'), '"\\\\"'); + assertEquals(angular.String.quote("'a'"), '"\'a\'"'); + assertEquals(angular.String.quote('"a"'), '"\\"a\\""'); + assertEquals(angular.String.quote('\n\f\r\t'), '"\\n\\f\\r\\t"'); +}; + +ApiTest.prototype.testQuoteStringBug = function(){ + assertEquals('"7\\\\\\\"7"', angular.String.quote("7\\\"7")); +}; + +ApiTest.prototype.testQuoteUnicode = function(){ + assertEquals('"abc\\u00a0def"', angular.String.quoteUnicode('abc\u00A0def')); +}; + +ApiTest.prototype.testMerge = function() { + var array = [{name:"misko"}]; + angular.Array.merge(array, 0, {name:"", email:"email1"}); + angular.Array.merge(array, 1, {name:"adam", email:"email2"}); + assertJsonEquals([{"email":"email1","name":"misko"},{"email":"email2","name":"adam"}], array); +}; + +ApiTest.prototype.testOrderByToggle = function() { + var orderByToggle = angular.Array.orderByToggle; + var predicate = []; + assertEquals(['+a'], orderByToggle(predicate, 'a')); + assertEquals(['-a'], orderByToggle(predicate, 'a')); + + assertEquals(['-a', '-b'], orderByToggle(['-b', 'a'], 'a')); +}; + +ApiTest.prototype.testOrderByToggle = function() { + var orderByDirection = angular.Array.orderByDirection; + assertEquals("", orderByDirection(['+a','b'], 'x')); + assertEquals("", orderByDirection(['+a','b'], 'b')); + assertEquals('ng-ascend', orderByDirection(['a','b'], 'a')); + assertEquals('ng-ascend', orderByDirection(['+a','b'], 'a')); + assertEquals('ng-descend', orderByDirection(['-a','b'], 'a')); + assertEquals('up', orderByDirection(['+a','b'], 'a', 'up', 'down')); + assertEquals('down', orderByDirection(['-a','b'], 'a', 'up', 'down')); +}; + +ApiTest.prototype.testDateToUTC = function(){ + var date = new Date("Sep 10 2003 13:02:03 GMT"); + assertEquals("date", angular.Object.typeOf(date)); + assertEquals("2003-09-10T13:02:03Z", angular.Date.toString(date)); +}; + +ApiTest.prototype.testStringFromUTC = function(){ + var date = angular.String.toDate("2003-09-10T13:02:03Z"); + assertEquals("date", angular.Object.typeOf(date)); + assertEquals("2003-09-10T13:02:03Z", angular.Date.toString(date)); + assertEquals("str", angular.String.toDate("str")); +}; + +ApiTest.prototype.testObjectShouldHaveExtend = function(){ + assertEquals({a:1, b:2}, angular.Object.extend({a:1}, {b:2})); +}; diff --git a/test/BinderTest.js b/test/BinderTest.js new file mode 100644 index 00000000..ecdd506f --- /dev/null +++ b/test/BinderTest.js @@ -0,0 +1,676 @@ +BinderTest = TestCase('BinderTest'); + +BinderTest.prototype.setUp = function(){ + var self = this; + this.compile = function(html, initialScope, config) { + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); + var element = self.element = jqLite(html); + var scope = compiler.compile(element)(element); + extend(scope, initialScope); + scope.$init(); + return {node:element, scope:scope}; + }; + this.compileToHtml = function (content) { + return sortedHtml(this.compile(content).node); + }; +}; + +BinderTest.prototype.tearDown = function(){ + if (this.element && this.element.dealoc) { + this.element.dealoc(); + } +}; + + +BinderTest.prototype.testChangingTextfieldUpdatesModel = function(){ + var state = this.compile('', {model:{}}); + state.scope.$eval(); + assertEquals('abc', state.scope.model.price); +}; + +BinderTest.prototype.testChangingTextareaUpdatesModel = function(){ + var c = this.compile(''); + c.scope.$eval(); + assertEquals(c.scope.model.note, 'abc'); +}; + +BinderTest.prototype.testChangingRadioUpdatesModel = function(){ + var c = this.compile('' + + ''); + c.scope.$eval(); + assertEquals(c.scope.model.price, 'A'); +}; + +BinderTest.prototype.testChangingCheckboxUpdatesModel = function(){ + var form = this.compile(''); + assertEquals(true, form.scope.model.price); +}; + +BinderTest.prototype.testBindUpdate = function() { + var c = this.compile('
      '); + assertEquals(123, c.scope.$get('a')); +}; + +BinderTest.prototype.testChangingSelectNonSelectedUpdatesModel = function(){ + var form = this.compile(''); + assertEquals('A', form.scope.model.price); +}; + +BinderTest.prototype.testChangingMultiselectUpdatesModel = function(){ + var form = this.compile(''); + assertJsonEquals(["A", "B"], form.scope.$get('Invoice').options); +}; + +BinderTest.prototype.testChangingSelectSelectedUpdatesModel = function(){ + var form = this.compile(''); + assertEquals(form.scope.model.price, 'b'); +}; + +BinderTest.prototype.testExecuteInitialization = function() { + var c = this.compile('
      '); + assertEquals(c.scope.$get('a'), 123); +}; + +BinderTest.prototype.testExecuteInitializationStatements = function() { + var c = this.compile('
      '); + assertEquals(c.scope.$get('a'), 123); + assertEquals(c.scope.$get('b'), 345); +}; + +BinderTest.prototype.testApplyTextBindings = function(){ + var form = this.compile('
      x
      '); + form.scope.$set('model', {a:123}); + form.scope.$eval(); + assertEquals('123', form.node.text()); +}; + +BinderTest.prototype.testReplaceBindingInTextWithSpan = function() { + assertEquals(this.compileToHtml("a{{b}}c"), 'ac'); + assertEquals(this.compileToHtml("{{b}}"), ''); +}; + +BinderTest.prototype.testBindingSpaceConfusesIE = function() { + if (!msie) return; + var span = document.createElement("span"); + span.innerHTML = ' '; + var nbsp = span.firstChild.nodeValue; + assertEquals( + ''+nbsp+'', + this.compileToHtml("{{a}} {{b}}")); + assertEquals( + ''+nbsp+'x '+nbsp+'()', + this.compileToHtml("{{A}} x {{B}} ({{C}})")); +}; + +BinderTest.prototype.testBindingOfAttributes = function() { + var c = this.compile(""); + var attrbinding = c.node.attr("ng-bind-attr"); + var bindings = fromJson(attrbinding); + assertEquals("http://s/a{{b}}c", decodeURI(bindings.href)); + assertTrue(!bindings.foo); +}; + +BinderTest.prototype.testMarkMultipleAttributes = function() { + var c = this.compile(''); + var attrbinding = c.node.attr("ng-bind-attr"); + var bindings = fromJson(attrbinding); + assertEquals(bindings.foo, "{{d}}"); + assertEquals(decodeURI(bindings.href), "http://s/a{{b}}c"); +}; + +BinderTest.prototype.testAttributesNoneBound = function() { + var c = this.compile(""); + var a = c.node; + assertEquals(a[0].nodeName, "A"); + assertTrue(!a.attr("ng-bind-attr")); +}; + +BinderTest.prototype.testExistingAttrbindingIsAppended = function() { + var c = this.compile(""); + var a = c.node; + assertEquals('{"b":"{{def}}","href":"http://s/{{abc}}"}', a.attr('ng-bind-attr')); +}; + +BinderTest.prototype.testAttributesAreEvaluated = function(){ + var c = this.compile(''); + var binder = c.binder, form = c.node; + c.scope.$eval('a=1;b=2'); + c.scope.$eval(); + var a = c.node; + assertEquals(a.attr('a'), 'a'); + assertEquals(a.attr('b'), 'a+b=3'); +}; + +BinderTest.prototype.testInputTypeButtonActionExecutesInScope = function(){ + var savedCalled = false; + var c = this.compile(''); + c.scope.$set("person.save", function(){ + savedCalled = true; + }); + c.node.trigger('click'); + assertTrue(savedCalled); +}; + +BinderTest.prototype.testInputTypeButtonActionExecutesInScope2 = function(){ + var log = ""; + var c = this.compile(''); + c.scope.$set("action", function(){ + log += 'click;'; + }); + expect(log).toEqual(''); + c.node.trigger('click'); + expect(log).toEqual('click;'); +}; + +BinderTest.prototype.testButtonElementActionExecutesInScope = function(){ + var savedCalled = false; + var c = this.compile(''); + c.scope.$set("person.save", function(){ + savedCalled = true; + }); + c.node.trigger('click'); + assertTrue(savedCalled); +}; + +BinderTest.prototype.testRepeaterUpdateBindings = function(){ + var a = this.compile('
      '); + var form = a.node; + var items = [{a:"A"}, {a:"B"}]; + a.scope.$set('model', {items:items}); + + a.scope.$eval(); + assertEquals('
        ' + + '<#comment>' + + '
      • A
      • ' + + '
      • B
      • ' + + '
      ', sortedHtml(form)); + + items.unshift({a:'C'}); + a.scope.$eval(); + assertEquals('
        ' + + '<#comment>' + + '
      • C
      • ' + + '
      • A
      • ' + + '
      • B
      • ' + + '
      ', sortedHtml(form)); + + items.shift(); + a.scope.$eval(); + assertEquals('
        ' + + '<#comment>' + + '
      • A
      • ' + + '
      • B
      • ' + + '
      ', sortedHtml(form)); + + items.shift(); + items.shift(); + a.scope.$eval(); +}; + +BinderTest.prototype.testRepeaterContentDoesNotBind = function(){ + var a = this.compile('
      '); + a.scope.$set('model', {items:[{a:"A"}]}); + a.scope.$eval(); + assertEquals('
        ' + + '<#comment>' + + '
      • A
      • ' + + '
      ', sortedHtml(a.node)); +}; + +BinderTest.prototype.testExpandEntityTag = function(){ + assertEquals( + '
      ', + this.compileToHtml('
      ')); +}; + +BinderTest.prototype.testDoNotOverwriteCustomAction = function(){ + var html = this.compileToHtml(''); + assertTrue(html.indexOf('action="foo();"') > 0 ); +}; + +BinderTest.prototype.testRepeaterAdd = function(){ + var c = this.compile('
      '); + var doc = c.node; + c.scope.$set('items', [{x:'a'}, {x:'b'}]); + c.scope.$eval(); + var first = childNode(c.node, 1); + var second = childNode(c.node, 2); + assertEquals('a', first.val()); + assertEquals('b', second.val()); + + first.val('ABC'); + first.trigger('keyup'); + assertEquals(c.scope.items[0].x, 'ABC'); +}; + +BinderTest.prototype.testItShouldRemoveExtraChildrenWhenIteratingOverHash = function(){ + var c = this.compile('
      {{i}}
      '); + var items = {}; + c.scope.$set("items", items); + + c.scope.$eval(); + expect(c.node[0].childNodes.length - 1).toEqual(0); + + items.name = "misko"; + c.scope.$eval(); + expect(c.node[0].childNodes.length - 1).toEqual(1); + + delete items.name; + c.scope.$eval(); + expect(c.node[0].childNodes.length - 1).toEqual(0); +}; + +BinderTest.prototype.testIfTextBindingThrowsErrorDecorateTheSpan = function(){ + var a = this.compile('
      {{error.throw()}}
      '); + var doc = a.node; + + a.scope.$set('error.throw', function(){throw "ErrorMsg1";}); + a.scope.$eval(); + var span = childNode(doc, 0); + assertTrue(span.hasClass('ng-exception')); + assertEquals('ErrorMsg1', fromJson(span.text())); + assertEquals('"ErrorMsg1"', span.attr('ng-exception')); + + a.scope.$set('error.throw', function(){throw "MyError";}); + a.scope.$eval(); + span = childNode(doc, 0); + assertTrue(span.hasClass('ng-exception')); + assertTrue(span.text(), span.text().match('MyError') !== null); + assertEquals('"MyError"', span.attr('ng-exception')); + + a.scope.$set('error.throw', function(){return "ok";}); + a.scope.$eval(); + assertFalse(span.hasClass('ng-exception')); + assertEquals('ok', span.text()); + assertEquals(null, span.attr('ng-exception')); +}; + +BinderTest.prototype.testIfAttrBindingThrowsErrorDecorateTheAttribute = function(){ + var a = this.compile('
      '); + var doc = a.node; + + a.scope.$set('error.throw', function(){throw "ErrorMsg";}); + a.scope.$eval(); + assertTrue('ng-exception', doc.hasClass('ng-exception')); + assertEquals('"ErrorMsg"', doc.attr('ng-exception')); + assertEquals('before "ErrorMsg" after', doc.attr('attr')); + + a.scope.$set('error.throw', function(){ return 'X';}); + a.scope.$eval(); + assertFalse('!ng-exception', doc.hasClass('ng-exception')); + assertEquals('before X after', doc.attr('attr')); + assertEquals(null, doc.attr('ng-exception')); + +}; + +BinderTest.prototype.testNestedRepeater = function() { + var a = this.compile('
      ' + + '
        ' + + '
        '); + + a.scope.$set('model', [{name:'a', item:['a1', 'a2']}, {name:'b', item:['b1', 'b2']}]); + a.scope.$eval(); + + assertEquals('
        '+ + '<#comment>'+ + '
        '+ + '<#comment>'+ + '
          '+ + '
            '+ + '
            '+ + '
            '+ + '<#comment>'+ + '
              '+ + '
                '+ + '
                ', sortedHtml(a.node)); +}; + +BinderTest.prototype.testHideBindingExpression = function() { + var a = this.compile('
                '); + + a.scope.$set('hidden', 3); + a.scope.$eval(); + + assertHidden(a.node); + + a.scope.$set('hidden', 2); + a.scope.$eval(); + + assertVisible(a.node); +}; + +BinderTest.prototype.testHideBinding = function() { + var c = this.compile('
                '); + + c.scope.$set('hidden', 'true'); + c.scope.$eval(); + + assertHidden(c.node); + + c.scope.$set('hidden', 'false'); + c.scope.$eval(); + + assertVisible(c.node); + + c.scope.$set('hidden', ''); + c.scope.$eval(); + + assertVisible(c.node); +}; + +BinderTest.prototype.testShowBinding = function() { + var c = this.compile('
                '); + + c.scope.$set('show', 'true'); + c.scope.$eval(); + + assertVisible(c.node); + + c.scope.$set('show', 'false'); + c.scope.$eval(); + + assertHidden(c.node); + + c.scope.$set('show', ''); + c.scope.$eval(); + + assertHidden(c.node); +}; + +BinderTest.prototype.testBindClassUndefined = function() { + var doc = this.compile('
                '); + doc.scope.$eval(); + + assertEquals( + '
                ', + sortedHtml(doc.node)); +}; + +BinderTest.prototype.testBindClass = function() { + var c = this.compile('
                '); + + c.scope.$set('class', 'testClass'); + c.scope.$eval(); + + assertEquals(sortedHtml(c.node), + '
                '); + + c.scope.$set('class', ['a', 'b']); + c.scope.$eval(); + + assertEquals(sortedHtml(c.node), + '
                '); +}; + +BinderTest.prototype.testBindClassEvenOdd = function() { + var x = this.compile('
                '); + x.scope.$eval(); + assertEquals( + '
                <#comment>' + + '
                ' + + '
                ', + sortedHtml(x.node)); +}; + +BinderTest.prototype.testBindStyle = function() { + var c = this.compile('
                '); + + c.scope.$eval('style={color:"red"}'); + c.scope.$eval(); + + assertEquals("red", c.node.css('color')); + + c.scope.$eval('style={}'); + c.scope.$eval(); +}; + +BinderTest.prototype.testActionOnAHrefThrowsError = function(){ + var model = {books:[]}; + var c = this.compile('Add Phone', model); + c.scope.action = function(){ + throw {a:'abc', b:2}; + }; + var input = c.node; + input.trigger('click'); + var error = fromJson(input.attr('ng-exception')); + assertEquals("abc", error.a); + assertEquals(2, error.b); + assertTrue("should have an error class", input.hasClass('ng-exception')); + + // TODO: I think that exception should never get cleared so this portion of test makes no sense + //c.scope.action = noop; + //input.trigger('click'); + //dump(input.attr('ng-error')); + //assertFalse('error class should be cleared', input.hasClass('ng-exception')); +}; + +BinderTest.prototype.testShoulIgnoreVbNonBindable = function(){ + var c = this.compile("
                {{a}}" + + "
                {{a}}
                " + + "
                {{b}}
                " + + "
                {{c}}
                "); + c.scope.$set('a', 123); + c.scope.$eval(); + assertEquals('123{{a}}{{b}}{{c}}', c.node.text()); +}; + +BinderTest.prototype.testOptionShouldUpdateParentToGetProperBinding = function() { + var c = this.compile(''); + c.scope.$set('s', 1); + c.scope.$eval(); + assertEquals(1, c.node[0].selectedIndex); +}; + +BinderTest.prototype.testRepeaterShouldBindInputsDefaults = function () { + var c = this.compile('
                '); + c.scope.$set('items', [{}, {name:'misko'}]); + c.scope.$eval(); + + assertEquals("123", c.scope.$eval('items[0].name')); + assertEquals("misko", c.scope.$eval('items[1].name')); +}; + +BinderTest.prototype.testRepeaterShouldCreateArray = function () { + var c = this.compile(''); + c.scope.$eval(); + + assertEquals(0, c.scope.$get('items').length); +}; + +BinderTest.prototype.testShouldTemplateBindPreElements = function () { + var c = this.compile('
                Hello {{name}}!
                '); + c.scope.$set("name", "World"); + c.scope.$eval(); + + assertEquals('
                Hello World!
                ', sortedHtml(c.node)); +}; + +BinderTest.prototype.testFillInOptionValueWhenMissing = function() { + var c = this.compile( + ''); + c.scope.$set('a', 'A'); + c.scope.$set('b', 'B'); + c.scope.$eval(); + var optionA = childNode(c.node, 0); + var optionB = childNode(c.node, 1); + var optionC = childNode(c.node, 2); + + expect(optionA.attr('value')).toEqual('A'); + expect(optionA.text()).toEqual('A'); + + expect(optionB.attr('value')).toEqual(''); + expect(optionB.text()).toEqual('B'); + + expect(optionC.attr('value')).toEqual('C'); + expect(optionC.text()).toEqual('C'); +}; + +BinderTest.prototype.testValidateForm = function() { + var c = this.compile('
                ' + + '
                '); + var items = [{}, {}]; + c.scope.$set("items", items); + c.scope.$eval(); + assertEquals(3, c.scope.$get("$invalidWidgets.length")); + + c.scope.$set('name', ''); + c.scope.$eval(); + assertEquals(3, c.scope.$get("$invalidWidgets.length")); + + c.scope.$set('name', ' '); + c.scope.$eval(); + assertEquals(3, c.scope.$get("$invalidWidgets.length")); + + c.scope.$set('name', 'abc'); + c.scope.$eval(); + assertEquals(2, c.scope.$get("$invalidWidgets.length")); + + items[0].name = 'abc'; + c.scope.$eval(); + assertEquals(1, c.scope.$get("$invalidWidgets.length")); + + items[1].name = 'abc'; + c.scope.$eval(); + assertEquals(0, c.scope.$get("$invalidWidgets.length")); +}; + +BinderTest.prototype.testValidateOnlyVisibleItems = function(){ + var c = this.compile('
                '); + jqLite(document.body).append(c.node); + c.scope.$set("show", true); + c.scope.$eval(); + assertEquals(2, c.scope.$get("$invalidWidgets.length")); + + c.scope.$set("show", false); + c.scope.$eval(); + assertEquals(1, c.scope.$invalidWidgets.visible()); +}; + +BinderTest.prototype.testDeleteAttributeIfEvaluatesFalse = function() { + var c = this.compile('
                ' + + '' + + '' + + '
                '); + c.scope.$eval(); + function assertChild(index, disabled) { + var child = childNode(c.node, index); + assertEquals(sortedHtml(child), disabled, !!child.attr('disabled')); + } + + assertChild(0, true); + assertChild(1, false); + assertChild(2, true); + assertChild(3, false); + assertChild(4, true); + assertChild(5, false); +}; + +BinderTest.prototype.testItShouldDisplayErrorWhenActionIsSyntacticlyIncorect = function(){ + var c = this.compile('
                ' + + '' + + '
                '); + var first = jqLite(c.node[0].childNodes[0]); + var second = jqLite(c.node[0].childNodes[1]); + + first.trigger('click'); + assertEquals("ABC", c.scope.greeting); + + second.trigger('click'); + assertTrue(second.hasClass("ng-exception")); +}; + +BinderTest.prototype.testItShouldSelectTheCorrectRadioBox = function() { + var c = this.compile('
                ' + + '' + + '
                '); + var female = jqLite(c.node[0].childNodes[0]); + var male = jqLite(c.node[0].childNodes[1]); + + click(female); + assertEquals("female", c.scope.sex); + assertEquals(true, female[0].checked); + assertEquals(false, male[0].checked); + assertEquals("female", female.val()); + + click(male); + assertEquals("male", c.scope.sex); + assertEquals(false, female[0].checked); + assertEquals(true, male[0].checked); + assertEquals("male", male.val()); +}; + +BinderTest.prototype.testItShouldListenOnRightScope = function() { + var c = this.compile( + '
                  ' + + '
                '); + c.scope.$eval(); + assertEquals(0, c.scope.$get("counter")); + assertEquals(0, c.scope.$get("gCounter")); + + c.scope.$set("w", "something"); + c.scope.$eval(); + assertEquals(1, c.scope.$get("counter")); + assertEquals(7, c.scope.$get("gCounter")); +}; + +BinderTest.prototype.testItShouldRepeatOnHashes = function() { + var x = this.compile('
                '); + x.scope.$eval(); + assertEquals('
                  ' + + '<#comment>' + + '
                • a0
                • ' + + '
                • b1
                • ' + + '
                ', + sortedHtml(x.node)); +}; + +BinderTest.prototype.testItShouldFireChangeListenersBeforeUpdate = function(){ + var x = this.compile('
                '); + x.scope.$set("name", ""); + x.scope.$watch("watched", "name=123"); + x.scope.$set("watched", "change"); + x.scope.$eval(); + assertEquals(123, x.scope.$get("name")); + assertEquals( + '
                123
                ', + sortedHtml(x.node)); +}; + +BinderTest.prototype.testItShouldHandleMultilineBindings = function(){ + var x = this.compile('
                {{\n 1 \n + \n 2 \n}}
                '); + x.scope.$eval(); + assertEquals("3", x.node.text()); +}; + +BinderTest.prototype.testItBindHiddenInputFields = function(){ + var x = this.compile(''); + x.scope.$eval(); + assertEquals("abc", x.scope.$get("myName")); +}; + +BinderTest.prototype.XtestItShouldRenderMultiRootHtmlInBinding = function() { + var x = this.compile('
                before {{a|html}}after
                '); + x.scope.a = "acd"; + x.scope.$eval(); + assertEquals( + '
                before acdafter
                ', + sortedHtml(x.node)); +}; + +BinderTest.prototype.testItShouldUseFormaterForText = function() { + var x = this.compile(''); + x.scope.$eval(); + assertEquals(['a','b'], x.scope.$get('a')); + var input = x.node; + input[0].value = ' x,,yz'; + input.trigger('change'); + assertEquals(['x','yz'], x.scope.$get('a')); + x.scope.$set('a', [1 ,2, 3]); + x.scope.$eval(); + assertEquals('1, 2, 3', input[0].value); +}; + diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js new file mode 100644 index 00000000..3ce158b4 --- /dev/null +++ b/test/BrowserSpecs.js @@ -0,0 +1,48 @@ +describe('browser', function(){ + + var browser, location; + + beforeEach(function(){ + location = {href:"http://server", hash:""}; + browser = new Browser(location, {}); + browser.setTimeout = noop; + }); + + it('should watch url', function(){ + browser.delay = 1; + expectAsserts(2); + browser.watchUrl(function(url){ + assertEquals('http://getangular.test', url); + }); + browser.setTimeout = function(fn, delay){ + assertEquals(1, delay); + location.href = "http://getangular.test"; + browser.setTimeout = function(fn, delay) {}; + fn(); + }; + browser.startUrlWatcher(); + }); + + describe('outstading requests', function(){ + it('should process callbacks immedietly with no outstanding requests', function(){ + var callback = jasmine.createSpy('callback'); + browser.notifyWhenNoOutstandingRequests(callback); + expect(callback).wasCalled(); + }); + + it('should queue callbacks with outstanding requests', function(){ + var callback = jasmine.createSpy('callback'); + browser.outstandingRequests.count = 1; + browser.notifyWhenNoOutstandingRequests(callback); + expect(callback).not.wasCalled(); + + browser.processRequestCallbacks(); + expect(callback).not.wasCalled(); + + browser.outstandingRequests.count = 0; + browser.processRequestCallbacks(); + expect(callback).wasCalled(); + }); + }); + +}); diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js new file mode 100644 index 00000000..da354ea5 --- /dev/null +++ b/test/CompilerSpec.js @@ -0,0 +1,137 @@ +describe('compiler', function(){ + var compiler, textMarkup, directives, widgets, compile, log; + + beforeEach(function(){ + log = ""; + directives = { + hello: function(expression, element){ + log += "hello "; + return function() { + log += expression; + }; + }, + + watch: function(expression, element){ + return function() { + this.$watch(expression, function(val){ + log += ":" + val; + }); + }; + } + + }; + textMarkup = []; + attrMarkup = []; + widgets = {}; + compiler = new Compiler(textMarkup, attrMarkup, directives, widgets); + compile = function(html){ + var e = jqLite("
                " + html + "
                "); + var scope = compiler.compile(e)(e); + scope.$init(); + return scope; + }; + }); + + it('should recognize a directive', function(){ + var e = jqLite('
                '); + directives.directive = function(expression, element){ + log += "found"; + expect(expression).toEqual("expr"); + expect(element).toEqual(e); + return function initFn() { + log += ":init"; + }; + }; + var template = compiler.compile(e); + var init = template(e).$init; + expect(log).toEqual("found"); + init(); + expect(log).toEqual("found:init"); + }); + + it('should recurse to children', function(){ + var scope = compile('
                '); + expect(log).toEqual("hello misko"); + }); + + it('should watch scope', function(){ + var scope = compile(''); + expect(log).toEqual(""); + scope.$eval(); + scope.$set('name', 'misko'); + scope.$eval(); + scope.$eval(); + scope.$set('name', 'adam'); + scope.$eval(); + scope.$eval(); + expect(log).toEqual(":misko:adam"); + }); + + it('should prevent descend', function(){ + directives.stop = function(){ this.descend(false); }; + var scope = compile(''); + expect(log).toEqual("hello misko"); + }); + + it('should allow creation of templates', function(){ + directives.duplicate = function(expr, element){ + var parent = element.parent(); + element.replaceWith(document.createComment("marker")); + element.removeAttr("duplicate"); + var template = this.compile(element); + return function(marker) { + this.$onEval(function() { + marker.after(template(element.clone()).$element); + }); + }; + }; + var scope = compile('beforexafter'); + expect(sortedHtml(scope.$element)).toEqual('
                before<#comment>xafter
                '); + scope.$eval(); + expect(sortedHtml(scope.$element)).toEqual('
                before<#comment>xxafter
                '); + scope.$eval(); + expect(sortedHtml(scope.$element)).toEqual('
                before<#comment>xxxafter
                '); + }); + + it('should process markup before directives', function(){ + textMarkup.push(function(text, textNode, parentNode) { + if (text == 'middle') { + expect(textNode.text()).toEqual(text); + parentNode.attr('hello', text); + textNode[0].nodeValue = 'replaced'; + } + }); + var scope = compile('beforemiddleafter'); + expect(lowercase(scope.$element[0].innerHTML)).toEqual('beforereplacedafter'); + expect(log).toEqual("hello middle"); + }); + + it('should replace widgets', function(){ + widgets['NG:BUTTON'] = function(element) { + element.replaceWith('
                button
                '); + return function(element) { + log += 'init'; + }; + }; + var scope = compile('push me'); + expect(lowercase(scope.$element[0].innerHTML)).toEqual('
                button
                '); + expect(log).toEqual('init'); + }); + + it('should use the replaced element after calling widget', function(){ + widgets['H1'] = function(element) { + var span = angular.element('{{1+2}}'); + element.replaceWith(span); + this.descend(true); + this.directives(true); + return noop; + }; + textMarkup.push(function(text, textNode, parent){ + if (text == '{{1+2}}') + parent.text('3'); + }); + var scope = compile('

                ignore me

                '); + expect(scope.$element.text()).toEqual('3'); + }); + +}); diff --git a/test/ConsoleTest.js b/test/ConsoleTest.js new file mode 100644 index 00000000..3e09267b --- /dev/null +++ b/test/ConsoleTest.js @@ -0,0 +1,12 @@ +ConsoleTest = TestCase('ConsoleTest'); + +ConsoleTest.prototype.XtestConsoleWrite = function(){ + var consoleNode = jqLite("
                ")[0]; + consoleLog("error", ["Hello", "world"]); + assertEquals(jqLite(consoleNode)[0].nodeName, 'DIV'); + assertEquals(jqLite(consoleNode).text(), 'Hello world'); + assertEquals(jqLite(consoleNode.childNodes[0])[0].className, 'error'); + consoleLog("error",["Bye"]); + assertEquals(jqLite(consoleNode).text(), 'Hello worldBye'); + consoleNode = null; +}; diff --git a/test/FiltersTest.js b/test/FiltersTest.js new file mode 100644 index 00000000..f839bb51 --- /dev/null +++ b/test/FiltersTest.js @@ -0,0 +1,143 @@ +FiltersTest = TestCase('FiltersTest'); + +FiltersTest.prototype.testCurrency = function(){ + var html = jqLite(''); + var context = {$element:html}; + var currency = bind(context, angular.filter.currency); + + assertEquals(currency(0), '$0.00'); + assertEquals(html.hasClass('ng-format-negative'), false); + assertEquals(currency(-999), '$-999.00'); + assertEquals(html.hasClass('ng-format-negative'), true); + assertEquals(currency(1234.5678), '$1,234.57'); + assertEquals(html.hasClass('ng-format-negative'), false); +}; + +FiltersTest.prototype.testFilterThisIsContext = function(){ + expectAsserts(1); + var scope = createScope(); + scope.name = 'misko'; + angular.filter.testFn = function () { + assertEquals('scope not equal', 'misko', this.name); + }; + scope.$eval("0|testFn"); + delete angular.filter['testFn']; +}; + +FiltersTest.prototype.testNumberFormat = function(){ + var context = {jqElement:jqLite('')}; + var number = bind(context, angular.filter.number); + + assertEquals('0', number(0, 0)); + assertEquals('0.00', number(0)); + assertEquals('-999.00', number(-999)); + assertEquals('1,234.57', number(1234.5678)); + assertEquals('', number(Number.NaN)); + assertEquals('1,234.57', number("1234.5678")); + assertEquals("", number(1/0)); +}; + +FiltersTest.prototype.testJson = function () { + assertEquals(toJson({a:"b"}, true), angular.filter.json.call({$element:jqLite('
                ')}, {a:"b"})); +}; + +FiltersTest.prototype.testPackageTracking = function () { + var assert = function(title, trackingNo) { + var val = angular.filter.trackPackage(trackingNo, title); + assertNotNull("Did Not Match: " + trackingNo, val); + assertEquals(title + ": " + trim(trackingNo), val.text()); + assertNotNull(val.attr('href')); + }; + assert('UPS', ' 1Z 999 999 99 9999 999 9 '); + assert('UPS', '1ZW5w5220379084747'); + + assert('FedEx', '418822131061812'); + assert('FedEx', '9612019 5935 3267 2473 738'); + assert('FedEx', '9612019593532672473738'); + assert('FedEx', '235354667129449'); + assert('FedEx', '915368880571'); + assert('FedEx', '901712142390'); + assert('FedEx', '297391510063413'); + + assert('USPS', '9101 8052 1390 7402 4335 49'); + assert('USPS', '9101010521297963339560'); + assert('USPS', '9102901001301038667029'); + assert('USPS', '910 27974 4490 3000 8916 56'); + assert('USPS', '9102801438635051633253'); +}; + +FiltersTest.prototype.testLink = function() { + var assert = function(text, url, obj){ + var val = angular.filter.link(obj); + assertEquals('' + text + '', sortedHtml(val)); + }; + assert("url", "url", "url"); + assert("hello", "url", {text:"hello", url:"url"}); + assert("a@b.com", "mailto:a@b.com", "a@b.com"); +}; + +FiltersTest.prototype.testImage = function(){ + assertEquals(null, angular.filter.image()); + assertEquals(null, angular.filter.image({})); + assertEquals(null, angular.filter.image("")); + assertEquals('http://localhost/abc', angular.filter.image({url:"http://localhost/abc"}).attr('src')); +}; + +FiltersTest.prototype.testQRcode = function() { + assertEquals( + 'http://chart.apis.google.com/chart?chl=Hello%20world&chs=200x200&cht=qr', + angular.filter.qrcode('Hello world').attr('src')); +}; + +FiltersTest.prototype.testLowercase = function() { + assertEquals('abc', angular.filter.lowercase('AbC')); + assertEquals(null, angular.filter.lowercase(null)); +}; + +FiltersTest.prototype.testUppercase = function() { + assertEquals('ABC', angular.filter.uppercase('AbC')); + assertEquals(null, angular.filter.uppercase(null)); +}; + +FiltersTest.prototype.testLineCount = function() { + assertEquals(1, angular.filter.linecount(null)); + assertEquals(1, angular.filter.linecount('')); + assertEquals(1, angular.filter.linecount('a')); + assertEquals(2, angular.filter.linecount('a\nb')); + assertEquals(3, angular.filter.linecount('a\nb\nc')); +}; + +FiltersTest.prototype.testIf = function() { + assertEquals('A', angular.filter['if']('A', true)); + assertEquals(undefined, angular.filter['if']('A', false)); +}; + +FiltersTest.prototype.testUnless = function() { + assertEquals('A', angular.filter.unless('A', false)); + assertEquals(undefined, angular.filter.unless('A', true)); +}; + +FiltersTest.prototype.testGoogleChartApiEncode = function() { + assertEquals( + 'http://chart.apis.google.com/chart?chl=Hello world&chs=200x200&cht=qr', + angular.filter.googleChartApi.encode({cht:"qr", chl:"Hello world"}).attr('src')); +}; + +FiltersTest.prototype.testHtml = function() { + var html = angular.filter.html("acd"); + expect(html instanceof HTML).toBeTruthy(); + expect(html.html).toEqual("acd"); +}; + +FiltersTest.prototype.testLinky = function() { + var linky = angular.filter.linky; + assertEquals( + 'http://ab/ ' + + '(http://a/) ' + + '<http://a/> ' + + 'http://1.2/v:~-123. c', + linky("http://ab/ (http://a/) http://1.2/v:~-123. c").html); + assertEquals(undefined, linky(undefined)); +}; + + diff --git a/test/FormattersTest.js b/test/FormattersTest.js new file mode 100644 index 00000000..b520faf9 --- /dev/null +++ b/test/FormattersTest.js @@ -0,0 +1,37 @@ +TestCase("formatterTest", { + testNoop: function(){ + assertEquals("abc", angular.formatter.noop.format("abc")); + assertEquals("xyz", angular.formatter.noop.parse("xyz")); + assertEquals(null, angular.formatter.noop.parse(null)); + }, + + testList: function() { + assertEquals('a, b', angular.formatter.list.format(['a', 'b'])); + assertEquals('', angular.formatter.list.format([])); + assertEquals(['abc', 'c'], angular.formatter.list.parse(" , abc , c ,,")); + assertEquals([], angular.formatter.list.parse("")); + assertEquals([], angular.formatter.list.parse(null)); + }, + + testBoolean: function() { + assertEquals('true', angular.formatter['boolean'].format(true)); + assertEquals('false', angular.formatter['boolean'].format(false)); + assertEquals(true, angular.formatter['boolean'].parse("true")); + assertEquals(false, angular.formatter['boolean'].parse("")); + assertEquals(false, angular.formatter['boolean'].parse("false")); + assertEquals(null, angular.formatter['boolean'].parse(null)); + }, + + testNumber: function() { + assertEquals('1', angular.formatter.number.format(1)); + assertEquals(1, angular.formatter.number.format('1')); + }, + + testTrim: function() { + assertEquals('', angular.formatter.trim.format(null)); + assertEquals('', angular.formatter.trim.format("")); + assertEquals('a', angular.formatter.trim.format(" a ")); + assertEquals('a', angular.formatter.trim.parse(' a ')); + } + +}); diff --git a/test/JsonTest.js b/test/JsonTest.js new file mode 100644 index 00000000..1ed56da8 --- /dev/null +++ b/test/JsonTest.js @@ -0,0 +1,84 @@ +JsonTest = TestCase("JsonTest"); + +JsonTest.prototype.testPrimitives = function () { + assertEquals("null", toJson(0/0)); + assertEquals("null", toJson(null)); + assertEquals("true", toJson(true)); + assertEquals("false", toJson(false)); + assertEquals("123.45", toJson(123.45)); + assertEquals('"abc"', toJson("abc")); + assertEquals('"a \\t \\n \\r b \\\\"', toJson("a \t \n \r b \\")); +}; + +JsonTest.prototype.testEscaping = function () { + assertEquals("\"7\\\\\\\"7\"", toJson("7\\\"7")); +}; + +JsonTest.prototype.testObjects = function () { + assertEquals('{"a":1,"b":2}', toJson({a:1,b:2})); + assertEquals('{"a":{"b":2}}', toJson({a:{b:2}})); + assertEquals('{"a":{"b":{"c":0}}}', toJson({a:{b:{c:0}}})); + assertEquals('{"a":{"b":null}}', toJson({a:{b:0/0}})); +}; + +JsonTest.prototype.testObjectPretty = function () { + assertEquals('{\n "a":1,\n "b":2}', toJson({a:1,b:2}, true)); + assertEquals('{\n "a":{\n "b":2}}', toJson({a:{b:2}}, true)); +}; + +JsonTest.prototype.testArray = function () { + assertEquals('[]', toJson([])); + assertEquals('[1,"b"]', toJson([1,"b"])); +}; + +JsonTest.prototype.testIgnoreFunctions = function () { + assertEquals('[null,1]', toJson([function(){},1])); + assertEquals('{}', toJson({a:function(){}})); +}; + +JsonTest.prototype.testParseNull = function () { + assertNull(fromJson("null")); +}; + +JsonTest.prototype.testParseBoolean = function () { + assertTrue(fromJson("true")); + assertFalse(fromJson("false")); +}; + +JsonTest.prototype.test$$isIgnored = function () { + assertEquals("{}", toJson({$$:0})); +}; + +JsonTest.prototype.testArrayWithEmptyItems = function () { + var a = []; + a[1] = "X"; + assertEquals('[null,"X"]', toJson(a)); +}; + +JsonTest.prototype.testItShouldEscapeUnicode = function () { + assertEquals(1, "\u00a0".length); + assertEquals(8, toJson("\u00a0").length); + assertEquals(1, fromJson(toJson("\u00a0")).length); +}; + +JsonTest.prototype.testItShouldUTCDates = function() { + var date = angular.String.toDate("2009-10-09T01:02:03Z"); + assertEquals('"2009-10-09T01:02:03Z"', toJson(date)); + assertEquals(date.getTime(), + fromJson('"2009-10-09T01:02:03Z"').getTime()); +}; + +JsonTest.prototype.testItShouldPreventRecursion = function () { + var obj = {a:'b'}; + obj.recursion = obj; + assertEquals('{"a":"b","recursion":RECURSION}', angular.toJson(obj)); +}; + +JsonTest.prototype.testItShouldSerializeSameObjectsMultipleTimes = function () { + var obj = {a:'b'}; + assertEquals('{"A":{"a":"b"},"B":{"a":"b"}}', angular.toJson({A:obj, B:obj})); +}; + +JsonTest.prototype.testItShouldNotSerializeUndefinedValues = function () { + assertEquals('{}', angular.toJson({A:undefined})); +}; diff --git a/test/ParserTest.js b/test/ParserTest.js new file mode 100644 index 00000000..7ba65f18 --- /dev/null +++ b/test/ParserTest.js @@ -0,0 +1,465 @@ +LexerTest = TestCase('LexerTest'); + +LexerTest.prototype.testTokenizeAString = function(){ + var lexer = new Lexer("a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\""); + var tokens = lexer.parse(); + var i = 0; + assertEquals(tokens[i].index, 0); + assertEquals(tokens[i].text, 'a.bc'); + + i++; + assertEquals(tokens[i].index, 4); + assertEquals(tokens[i].text, '['); + + i++; + assertEquals(tokens[i].index, 5); + assertEquals(tokens[i].text, 22); + + i++; + assertEquals(tokens[i].index, 7); + assertEquals(tokens[i].text, ']'); + + i++; + assertEquals(tokens[i].index, 8); + assertEquals(tokens[i].text, '+'); + + i++; + assertEquals(tokens[i].index, 9); + assertEquals(tokens[i].text, 1.3); + + i++; + assertEquals(tokens[i].index, 12); + assertEquals(tokens[i].text, '|'); + + i++; + assertEquals(tokens[i].index, 13); + assertEquals(tokens[i].text, 'f'); + + i++; + assertEquals(tokens[i].index, 14); + assertEquals(tokens[i].text, ':'); + + i++; + assertEquals(tokens[i].index, 15); + assertEquals(tokens[i].string, "a'c"); + + i++; + assertEquals(tokens[i].index, 21); + assertEquals(tokens[i].text, ':'); + + i++; + assertEquals(tokens[i].index, 22); + assertEquals(tokens[i].string, 'd"e'); +}; + +LexerTest.prototype.testTokenizeUndefined = function(){ + var lexer = new Lexer("undefined"); + var tokens = lexer.parse(); + var i = 0; + assertEquals(tokens[i].index, 0); + assertEquals(tokens[i].text, 'undefined'); + assertEquals(undefined, tokens[i].fn()); +}; + + + +LexerTest.prototype.testTokenizeRegExp = function(){ + var lexer = new Lexer("/r 1/"); + var tokens = lexer.parse(); + var i = 0; + assertEquals(tokens[i].index, 0); + assertEquals(tokens[i].text, 'r 1'); + assertEquals("r 1".match(tokens[i].fn())[0], 'r 1'); +}; + +LexerTest.prototype.testQuotedString = function(){ + var str = "['\\'', \"\\\"\"]"; + var lexer = new Lexer(str); + var tokens = lexer.parse(); + + assertEquals(1, tokens[1].index); + assertEquals("'", tokens[1].string); + + assertEquals(7, tokens[3].index); + assertEquals('"', tokens[3].string); + +}; + +LexerTest.prototype.testQuotedStringEscape = function(){ + var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"'; + var lexer = new Lexer(str); + var tokens = lexer.parse(); + + assertEquals('"\n\f\r\t\v\u00A0', tokens[0].string); +}; + +LexerTest.prototype.testTokenizeUnicode = function(){ + var lexer = new Lexer('"\\u00A0"'); + var tokens = lexer.parse(); + assertEquals(1, tokens.length); + assertEquals('\u00a0', tokens[0].string); +}; + +LexerTest.prototype.testTokenizeRegExpWithOptions = function(){ + var lexer = new Lexer("/r/g"); + var tokens = lexer.parse(); + var i = 0; + assertEquals(tokens[i].index, 0); + assertEquals(tokens[i].text, 'r'); + assertEquals(tokens[i].flags, 'g'); + assertEquals("rr".match(tokens[i].fn()).length, 2); +}; + +LexerTest.prototype.testTokenizeRegExpWithEscape = function(){ + var lexer = new Lexer("/\\/\\d/"); + var tokens = lexer.parse(); + var i = 0; + assertEquals(tokens[i].index, 0); + assertEquals(tokens[i].text, '\\/\\d'); + assertEquals("/1".match(tokens[i].fn())[0], '/1'); +}; + +LexerTest.prototype.testIgnoreWhitespace = function(){ + var lexer = new Lexer("a \t \n \r b"); + var tokens = lexer.parse(); + assertEquals(tokens[0].text, 'a'); + assertEquals(tokens[1].text, 'b'); +}; + +LexerTest.prototype.testRelation = function(){ + var lexer = new Lexer("! == != < > <= >="); + var tokens = lexer.parse(); + assertEquals(tokens[0].text, '!'); + assertEquals(tokens[1].text, '=='); + assertEquals(tokens[2].text, '!='); + assertEquals(tokens[3].text, '<'); + assertEquals(tokens[4].text, '>'); + assertEquals(tokens[5].text, '<='); + assertEquals(tokens[6].text, '>='); +}; + +LexerTest.prototype.testStatements = function(){ + var lexer = new Lexer("a;b;"); + var tokens = lexer.parse(); + assertEquals(tokens[0].text, 'a'); + assertEquals(tokens[1].text, ';'); + assertEquals(tokens[2].text, 'b'); + assertEquals(tokens[3].text, ';'); +}; + +ParserTest = TestCase('ParserTest'); + +ParserTest.prototype.testExpressions = function(){ + var scope = createScope(); + assertEquals(scope.$eval("-1"), -1); + assertEquals(scope.$eval("1 + 2.5"), 3.5); + assertEquals(scope.$eval("1 + -2.5"), -1.5); + assertEquals(scope.$eval("1+2*3/4"), 1+2*3/4); + assertEquals(scope.$eval("0--1+1.5"), 0- -1 + 1.5); + assertEquals(scope.$eval("-0--1++2*-3/-4"), -0- -1+ +2*-3/-4); + assertEquals(scope.$eval("1/2*3"), 1/2*3); +}; + +ParserTest.prototype.testComparison = function(){ + var scope = createScope(); + assertEquals(scope.$eval("false"), false); + assertEquals(scope.$eval("!true"), false); + assertEquals(scope.$eval("1==1"), true); + assertEquals(scope.$eval("1!=2"), true); + assertEquals(scope.$eval("1<2"), true); + assertEquals(scope.$eval("1<=1"), true); + assertEquals(scope.$eval("1>2"), 1>2); + assertEquals(scope.$eval("2>=1"), 2>=1); + + assertEquals(true === 2<3, scope.$eval("true==2<3")); + +}; + +ParserTest.prototype.testLogical = function(){ + var scope = createScope(); + assertEquals(scope.$eval("0&&2"), 0&&2); + assertEquals(scope.$eval("0||2"), 0||2); + assertEquals(scope.$eval("0||1&&2"), 0||1&&2); +}; + +ParserTest.prototype.testString = function(){ + var scope = createScope(); + assertEquals(scope.$eval("'a' + 'b c'"), "ab c"); +}; + +ParserTest.prototype.testFilters = function(){ + angular.filter.substring = function(input, start, end) { + return input.substring(start, end); + }; + + angular.filter.upper = {_case:function(input) { + return input.toUpperCase(); + }}; + var scope = createScope(); + try { + scope.$eval("1|nonExistant"); + fail(); + } catch (e) { + assertEquals(e, "Function 'nonExistant' at column '3' in '1|nonExistant' is not defined."); + } + scope.$set('offset', 3); + assertEquals(scope.$eval("'abcd'|upper._case"), "ABCD"); + assertEquals(scope.$eval("'abcd'|substring:1:offset"), "bc"); + assertEquals(scope.$eval("'abcd'|substring:1:3|upper._case"), "BC"); +}; + +ParserTest.prototype.testScopeAccess = function(){ + var scope = createScope(); + scope.$set('a', 123); + scope.$set('b.c', 456); + assertEquals(scope.$eval("a", scope), 123); + assertEquals(scope.$eval("b.c", scope), 456); + assertEquals(scope.$eval("x.y.z", scope), undefined); +}; + +ParserTest.prototype.testGrouping = function(){ + var scope = createScope(); + assertEquals(scope.$eval("(1+2)*3"), (1+2)*3); +}; + +ParserTest.prototype.testAssignments = function(){ + var scope = createScope(); + assertEquals(scope.$eval("a=12"), 12); + assertEquals(scope.$get("a"), 12); + + scope = createScope(); + assertEquals(scope.$eval("x.y.z=123;"), 123); + assertEquals(scope.$get("x.y.z"), 123); + + assertEquals(234, scope.$eval("a=123; b=234")); + assertEquals(123, scope.$get("a")); + assertEquals(234, scope.$get("b")); +}; + +ParserTest.prototype.testFunctionCallsNoArgs = function(){ + var scope = createScope(); + scope.$set('const', function(a,b){return 123;}); + assertEquals(scope.$eval("const()"), 123); +}; + +ParserTest.prototype.testFunctionCalls = function(){ + var scope = createScope(); + scope.$set('add', function(a,b){ + return a+b; + }); + assertEquals(3, scope.$eval("add(1,2)")); +}; + +ParserTest.prototype.testCalculationBug = function(){ + var scope = createScope(); + scope.$set('taxRate', 8); + scope.$set('subTotal', 100); + assertEquals(scope.$eval("taxRate / 100 * subTotal"), 8); + assertEquals(scope.$eval("subTotal * taxRate / 100"), 8); +}; + +ParserTest.prototype.testArray = function(){ + var scope = createScope(); + assertEquals(scope.$eval("[]").length, 0); + assertEquals(scope.$eval("[1, 2]").length, 2); + assertEquals(scope.$eval("[1, 2]")[0], 1); + assertEquals(scope.$eval("[1, 2]")[1], 2); +}; + +ParserTest.prototype.testArrayAccess = function(){ + var scope = createScope(); + assertEquals(scope.$eval("[1][0]"), 1); + assertEquals(scope.$eval("[[1]][0][0]"), 1); + assertEquals(scope.$eval("[].length"), 0); + assertEquals(scope.$eval("[1, 2].length"), 2); +}; + +ParserTest.prototype.testObject = function(){ + var scope = createScope(); + assertEquals(toJson(scope.$eval("{}")), "{}"); + assertEquals(toJson(scope.$eval("{a:'b'}")), '{"a":"b"}'); + assertEquals(toJson(scope.$eval("{'a':'b'}")), '{"a":"b"}'); + assertEquals(toJson(scope.$eval("{\"a\":'b'}")), '{"a":"b"}'); +}; + +ParserTest.prototype.testObjectAccess = function(){ + var scope = createScope(); + assertEquals("WC", scope.$eval("{false:'WC', true:'CC'}[false]")); +}; + +ParserTest.prototype.testJSON = function(){ + var scope = createScope(); + assertEquals(toJson(scope.$eval("[{}]")), "[{}]"); + assertEquals(toJson(scope.$eval("[{a:[]}, {b:1}]")), '[{"a":[]},{"b":1}]'); +}; + +ParserTest.prototype.testMultippleStatements = function(){ + var scope = createScope(); + assertEquals(scope.$eval("a=1;b=3;a+b"), 4); + assertEquals(scope.$eval(";;1;;"), 1); +}; + +ParserTest.prototype.testParseThrow = function(){ + expectAsserts(1); + var scope = createScope(); + scope.$set('e', 'abc'); + try { + scope.$eval("throw e"); + } catch(e) { + assertEquals(e, 'abc'); + } +}; + +ParserTest.prototype.testMethodsGetDispatchedWithCorrectThis = function(){ + var scope = createScope(); + var C = function (){ + this.a=123; + }; + C.prototype.getA = function(){ + return this.a; + }; + + scope.$set("obj", new C()); + assertEquals(123, scope.$eval("obj.getA()")); +}; +ParserTest.prototype.testMethodsArgumentsGetCorrectThis = function(){ + var scope = createScope(); + var C = function (){ + this.a=123; + }; + C.prototype.sum = function(value){ + return this.a + value; + }; + C.prototype.getA = function(){ + return this.a; + }; + + scope.$set("obj", new C()); + assertEquals(246, scope.$eval("obj.sum(obj.getA())")); +}; + +ParserTest.prototype.testObjectPointsToScopeValue = function(){ + var scope = createScope(); + scope.$set('a', "abc"); + assertEquals("abc", scope.$eval("{a:a}").a); +}; + +ParserTest.prototype.testFieldAccess = function(){ + var scope = createScope(); + var fn = function(){ + return {name:'misko'}; + }; + scope.$set('a', fn); + assertEquals("misko", scope.$eval("a().name")); +}; + +ParserTest.prototype.testArrayIndexBug = function () { + var scope = createScope(); + scope.$set('items', [{}, {name:'misko'}]); + + assertEquals("misko", scope.$eval('items[1].name')); +}; + +ParserTest.prototype.testArrayAssignment = function () { + var scope = createScope(); + scope.$set('items', []); + + assertEquals("abc", scope.$eval('items[1] = "abc"')); + assertEquals("abc", scope.$eval('items[1]')); +// Dont know how to make this work.... +// assertEquals("moby", scope.$eval('books[1] = "moby"')); +// assertEquals("moby", scope.$eval('books[1]')); +}; + +ParserTest.prototype.testFiltersCanBeGrouped = function () { + var scope = createScope({name:'MISKO'}); + assertEquals('misko', scope.$eval('n = (name|lowercase)')); + assertEquals('misko', scope.$eval('n')); +}; + +ParserTest.prototype.testFiltersCanBeGrouped = function () { + var scope = createScope({name:'MISKO'}); + assertEquals('misko', scope.$eval('n = (name|lowercase)')); + assertEquals('misko', scope.$eval('n')); +}; + +ParserTest.prototype.testRemainder = function () { + var scope = createScope(); + assertEquals(1, scope.$eval('1%2')); +}; + +ParserTest.prototype.testSumOfUndefinedIsNotUndefined = function () { + var scope = createScope(); + assertEquals(1, scope.$eval('1+undefined')); + assertEquals(1, scope.$eval('undefined+1')); +}; + +ParserTest.prototype.testMissingThrowsError = function() { + var scope = createScope(); + try { + scope.$eval('[].count('); + fail(); + } catch (e) { + assertEquals('Unexpected end of expression: [].count(', e); + } +}; + +ParserTest.prototype.testItShouldCreateClosureFunctionWithNoArguments = function () { + var scope = createScope(); + var fn = scope.$eval("{:value}"); + scope.$set("value", 1); + assertEquals(1, fn()); + scope.$set("value", 2); + assertEquals(2, fn()); + fn = scope.$eval("{():value}"); + assertEquals(2, fn()); +}; + +ParserTest.prototype.testItShouldCreateClosureFunctionWithArguments = function () { + var scope = createScope(); + scope.$set("value", 1); + var fn = scope.$eval("{(a):value+a}"); + assertEquals(11, fn(10)); + scope.$set("value", 2); + assertEquals(12, fn(10)); + fn = scope.$eval("{(a,b):value+a+b}"); + assertEquals(112, fn(10, 100)); +}; + +ParserTest.prototype.testItShouldHaveDefaultArugument = function(){ + var scope = createScope(); + var fn = scope.$eval("{:$*2}"); + assertEquals(4, fn(2)); +}; + +ParserTest.prototype.testDoubleNegationBug = function (){ + var scope = createScope(); + assertEquals(true, scope.$eval('true')); + assertEquals(false, scope.$eval('!true')); + assertEquals(true, scope.$eval('!!true')); + assertEquals('a', scope.$eval('{true:"a", false:"b"}[!!true]')); +}; + +ParserTest.prototype.testNegationBug = function () { + var scope = createScope(); + assertEquals(!false || true, scope.$eval("!false || true")); + assertEquals(!11 == 10, scope.$eval("!11 == 10")); + assertEquals(12/6/2, scope.$eval("12/6/2")); +}; + +ParserTest.prototype.testBugStringConfusesParser = function() { + var scope = createScope(); + assertEquals('!', scope.$eval('suffix = "!"')); +}; + +ParserTest.prototype.testParsingBug = function () { + var scope = createScope(); + assertEquals({a: "-"}, scope.$eval("{a:'-'}")); +}; + +ParserTest.prototype.testUndefined = function () { + var scope = createScope(); + assertEquals(undefined, scope.$eval("undefined")); + assertEquals(undefined, scope.$eval("a=undefined")); + assertEquals(undefined, scope.$get("a")); +}; diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js new file mode 100644 index 00000000..d11c3e08 --- /dev/null +++ b/test/ResourceSpec.js @@ -0,0 +1,159 @@ +describe("resource", function() { + var xhr, resource, CreditCard, callback; + + beforeEach(function(){ + var browser = new MockBrowser(); + xhr = browser.xhr; + resource = new ResourceFactory(xhr); + CreditCard = resource.route('/CreditCard/:id:verb', {id:'@id.key'}, { + charge:{ + method:'POST', + params:{verb:'!charge'} + } + }); + callback = jasmine.createSpy(); + }); + + it("should build resource", function(){ + expect(typeof CreditCard).toBe('function'); + expect(typeof CreditCard.get).toBe('function'); + expect(typeof CreditCard.save).toBe('function'); + expect(typeof CreditCard.remove).toBe('function'); + expect(typeof CreditCard['delete']).toBe('function'); + expect(typeof CreditCard.query).toBe('function'); + }); + + it('should default to empty parameters', function(){ + xhr.expectGET('URL').respond({}); + resource.route('URL').query(); + }); + + it("should build resource with default param", function(){ + xhr.expectGET('/Order/123/Line/456.visa?minimum=0.05').respond({id:'abc'}); + xhr.expectGET('/Order/123/Line/456.visa?minimum=0.05').respond({id:'ddd'}); + var LineItem = resource.route('/Order/:orderId/Line/:id:verb', {orderId: '123', id: '@id.key', verb:'.visa', minimum:0.05}); + var item = LineItem.get({id:456}); + xhr.flush(); + nakedExpect(item).toEqual({id:'abc'}); + + item = LineItem.get({id:456}); + xhr.flush(); + nakedExpect(item).toEqual({id:'abc'}); + + }); + + it("should create resource", function(){ + xhr.expectPOST('/CreditCard', {name:'misko'}).respond({id:123, name:'misko'}); + + var cc = CreditCard.save({name:'misko'}, callback); + nakedExpect(cc).toEqual({name:'misko'}); + expect(callback).wasNotCalled(); + xhr.flush(); + nakedExpect(cc).toEqual({id:123, name:'misko'}); + expect(callback).wasCalledWith(cc); + }); + + it("should read resource", function(){ + xhr.expectGET("/CreditCard/123").respond({id:123, number:'9876'}); + var cc = CreditCard.get({id:123}, callback); + expect(cc instanceof CreditCard).toBeTruthy(); + nakedExpect(cc).toEqual({}); + expect(callback).wasNotCalled(); + xhr.flush(); + nakedExpect(cc).toEqual({id:123, number:'9876'}); + expect(callback).wasCalledWith(cc); + }); + + it("should update resource", function(){ + xhr.expectPOST('/CreditCard/123', {id:{key:123}, name:'misko'}).respond({id:{key:123}, name:'rama'}); + + var cc = CreditCard.save({id:{key:123}, name:'misko'}, callback); + nakedExpect(cc).toEqual({id:{key:123}, name:'misko'}); + expect(callback).wasNotCalled(); + xhr.flush(); + }); + + it("should query resource", function(){ + xhr.expectGET("/CreditCard?key=value").respond([{id:1}, {id:2}]); + + var ccs = CreditCard.query({key:'value'}, callback); + expect(ccs).toEqual([]); + expect(callback).wasNotCalled(); + xhr.flush(); + nakedExpect(ccs).toEqual([{id:1}, {id:2}]); + expect(callback).wasCalledWith(ccs); + }); + + it("should have all arguments optional", function(){ + xhr.expectGET('/CreditCard').respond([{id:1}]); + var log = ''; + var ccs = CreditCard.query(function(){ log += 'cb;'; }); + xhr.flush(); + nakedExpect(ccs).toEqual([{id:1}]); + expect(log).toEqual('cb;'); + }); + + it('should delete resource', function(){ + xhr.expectDELETE("/CreditCard/123").respond({}); + + CreditCard.remove({id:123}, callback); + expect(callback).wasNotCalled(); + xhr.flush(); + nakedExpect(callback.mostRecentCall.args).toEqual([{}]); + }); + + it('should post charge verb', function(){ + xhr.expectPOST('/CreditCard/123!charge?amount=10', {auth:'abc'}).respond({success:'ok'}); + + CreditCard.charge({id:123, amount:10},{auth:'abc'}, callback); + }); + + it('should create on save', function(){ + xhr.expectPOST('/CreditCard', {name:'misko'}).respond({id:123}); + var cc = new CreditCard(); + expect(cc.$get).not.toBeDefined(); + expect(cc.$query).not.toBeDefined(); + expect(cc.$remove).toBeDefined(); + expect(cc.$save).toBeDefined(); + + cc.name = 'misko'; + cc.$save(callback); + nakedExpect(cc).toEqual({name:'misko'}); + xhr.flush(); + nakedExpect(cc).toEqual({id:123}); + expect(callback).wasCalledWith(cc); + }); + + it('should bind default parameters', function(){ + xhr.expectGET('/CreditCard/123.visa?minimum=0.05').respond({id:123}); + var Visa = CreditCard.bind({verb:'.visa', minimum:0.05}); + var visa = Visa.get({id:123}); + xhr.flush(); + nakedExpect(visa).toEqual({id:123}); + }); + + it('should excersize full stack', function(){ + var scope = angular.compile('
                '); + var Person = scope.$resource('/Person/:id'); + scope.$browser.xhr.expectGET('/Person/123').respond('\n{\nname:\n"misko"\n}\n'); + var person = Person.get({id:123}); + scope.$browser.xhr.flush(); + expect(person.name).toEqual('misko'); + }); + + describe('failure mode', function(){ + it('should report error when non 200', function(){ + xhr.expectGET('/CreditCard/123').respond(500, "Server Error"); + var cc = CreditCard.get({id:123}); + try { + xhr.flush(); + fail('expected exception, non thrown'); + } catch (e) { + expect(e.status).toEqual(500); + expect(e.response).toEqual('Server Error'); + expect(e.message).toEqual('500: Server Error'); + } + }); + }); + +}); diff --git a/test/ScenarioSpec.js b/test/ScenarioSpec.js new file mode 100644 index 00000000..9afe8e95 --- /dev/null +++ b/test/ScenarioSpec.js @@ -0,0 +1,51 @@ +describe("ScenarioSpec: Compilation", function(){ + it("should compile dom node and return scope", function(){ + var node = jqLite('
                {{b=a+1}}
                ')[0]; + var scope = compile(node); + scope.$init(); + expect(scope.a).toEqual(1); + expect(scope.b).toEqual(2); + }); + + it("should compile jQuery node and return scope", function(){ + var scope = compile(jqLite('
                {{a=123}}
                ')).$init(); + expect(jqLite(scope.$element).text()).toEqual('123'); + }); + + it("should compile text node and return scope", function(){ + var scope = compile('
                {{a=123}}
                ').$init(); + expect(jqLite(scope.$element).text()).toEqual('123'); + }); +}); + +describe("ScenarioSpec: Scope", function(){ + it("should have set, get, eval, $init, updateView methods", function(){ + var scope = compile('
                {{a}}
                ').$init(); + scope.$eval("$invalidWidgets.push({})"); + expect(scope.$set("a", 2)).toEqual(2); + expect(scope.$get("a")).toEqual(2); + expect(scope.$eval("a=3")).toEqual(3); + scope.$eval(); + expect(jqLite(scope.$element).text()).toEqual('3'); + }); + + it("should have $ objects", function(){ + var scope = compile('
                ', {$config: {a:"b"}}); + expect(scope.$get('$location')).toBeDefined(); + expect(scope.$get('$eval')).toBeDefined(); + expect(scope.$get('$config')).toBeDefined(); + expect(scope.$get('$config.a')).toEqual("b"); + }); +}); + +describe("ScenarioSpec: configuration", function(){ + it("should take location object", function(){ + var url = "http://server/#?book=moby"; + var scope = compile("
                {{$location}}
                "); + var $location = scope.$get('$location'); + expect($location.hashSearch.book).toBeUndefined(); + scope.$browser.setUrl(url); + scope.$browser.fireUrlWatchers(); + expect($location.hashSearch.book).toEqual('moby'); + }); +}); diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js new file mode 100644 index 00000000..d93400e5 --- /dev/null +++ b/test/ScopeSpec.js @@ -0,0 +1,181 @@ +describe('scope/model', function(){ + + it('should create a scope with parent', function(){ + var model = createScope({name:'Misko'}); + expect(model.name).toEqual('Misko'); + }); + + it('should have $get/set$/parent$', function(){ + var parent = {}; + var model = createScope(parent); + model.$set('name', 'adam'); + expect(model.name).toEqual('adam'); + expect(model.$get('name')).toEqual('adam'); + expect(model.$parent).toEqual(model); + expect(model.$root).toEqual(model); + }); + + describe('$eval', function(){ + it('should eval function with correct this and pass arguments', function(){ + var model = createScope(); + model.$eval(function(name){ + this.name = name; + }, 'works'); + expect(model.name).toEqual('works'); + }); + + it('should eval expression with correct this', function(){ + var model = createScope(); + model.$eval('name="works"'); + expect(model.name).toEqual('works'); + }); + + it('should do nothing on empty string and not update view', function(){ + var model = createScope(); + var onEval = jasmine.createSpy('onEval'); + model.$onEval(onEval); + model.$eval(''); + expect(onEval).wasNotCalled(); + }); + }); + + describe('$watch', function(){ + it('should watch an expression for change', function(){ + var model = createScope(); + model.oldValue = ""; + var nameCount = 0, evalCount = 0; + model.name = 'adam'; + model.$watch('name', function(){ nameCount ++; }); + model.$watch(function(){return model.name;}, function(newValue, oldValue){ + this.newValue = newValue; + this.oldValue = oldValue; + }); + model.$onEval(function(){evalCount ++;}); + model.name = 'misko'; + model.$eval(); + expect(nameCount).toEqual(2); + expect(evalCount).toEqual(1); + expect(model.newValue).toEqual('misko'); + expect(model.oldValue).toEqual('adam'); + }); + + it('should eval with no arguments', function(){ + var model = createScope(); + var count = 0; + model.$onEval(function(){count++;}); + model.$eval(); + expect(count).toEqual(1); + }); + }); + + describe('$bind', function(){ + it('should curry a function with respect to scope', function(){ + var model = createScope(); + model.name = 'misko'; + expect(model.$bind(function(){return this.name;})()).toEqual('misko'); + }); + }); + + describe('$tryEval', function(){ + it('should report error on element', function(){ + var scope = createScope(); + scope.$tryEval('throw "myerror";', function(error){ + scope.error = error; + }); + expect(scope.error).toEqual('myerror'); + }); + + it('should report error on visible element', function(){ + var element = jqLite('
                '); + var scope = createScope(); + scope.$tryEval('throw "myError"', element); + expect(element.attr('ng-exception')).toEqual('"myError"'); // errors are jsonified + expect(element.hasClass('ng-exception')).toBeTruthy(); + }); + + it('should report error on $excetionHandler', function(){ + var element = jqLite('
                '); + var scope = createScope(); + scope.$exceptionHandler = function(e){ + this.error = e; + }; + scope.$tryEval('throw "myError"'); + expect(scope.error).toEqual("myError"); + }); + }); + + // $onEval + describe('$onEval', function(){ + it("should eval using priority", function(){ + var scope = createScope(); + scope.log = ""; + scope.$onEval('log = log + "middle;"'); + scope.$onEval(-1, 'log = log + "first;"'); + scope.$onEval(1, 'log = log + "last;"'); + scope.$eval(); + expect(scope.log).toEqual('first;middle;last;'); + }); + + it("should have $root and $parent", function(){ + var parent = createScope(); + var scope = createScope(parent); + expect(scope.$root).toEqual(parent); + expect(scope.$parent).toEqual(parent); + }); + }); + + describe('service injection', function(){ + it('should inject services', function(){ + var scope = createScope(null, { + service:function(){ + return "ABC"; + } + }); + expect(scope.service).toEqual("ABC"); + }); + + it('should inject arugments', function(){ + var scope = createScope(null, { + name:function(){ + return "misko"; + }, + greet: extend(function(name) { + return 'hello ' + name; + }, {inject:['name']}) + }); + expect(scope.greet).toEqual("hello misko"); + }); + + it('should throw error on missing dependency', function(){ + try { + createScope(null, { + greet: extend(function(name) { + }, {inject:['name']}) + }); + } catch(e) { + expect(e).toEqual("Don't know how to inject 'name'."); + } + }); + }); + + describe('getterFn', function(){ + it('should get chain', function(){ + expect(getterFn('a.b')(undefined)).toEqual(undefined); + expect(getterFn('a.b')({})).toEqual(undefined); + expect(getterFn('a.b')({a:null})).toEqual(undefined); + expect(getterFn('a.b')({a:{}})).toEqual(undefined); + expect(getterFn('a.b')({a:{b:null}})).toEqual(null); + expect(getterFn('a.b')({a:{b:0}})).toEqual(0); + expect(getterFn('a.b')({a:{b:'abc'}})).toEqual('abc'); + }); + + it('should map type method on top of expression', function(){ + expect(getterFn('a.$filter')({a:[]})('')).toEqual([]); + }); + + it('should bind function this', function(){ + expect(getterFn('a')({a:function($){return this.b + $;}, b:1})(2)).toEqual(3); + + }); + }); +}); diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js new file mode 100644 index 00000000..573c340d --- /dev/null +++ b/test/ValidatorsTest.js @@ -0,0 +1,169 @@ +ValidatorTest = TestCase('ValidatorTest'); + +ValidatorTest.prototype.testItShouldHaveThisSet = function() { + var validator = {}; + angular.validator.myValidator = function(first, last){ + validator.first = first; + validator.last = last; + validator._this = this; + }; + var scope = compile(''); + scope.name = 'misko'; + scope.$init(); + assertEquals('misko', validator.first); + assertEquals('hevery', validator.last); + expect(validator._this.$id).toEqual(scope.$id); + delete angular.validator.myValidator; + scope.$element.remove(); +}; + +ValidatorTest.prototype.testRegexp = function() { + assertEquals(angular.validator.regexp("abc", /x/, "E1"), "E1"); + assertEquals(angular.validator.regexp("abc", '/x/'), + "Value does not match expected format /x/."); + assertEquals(angular.validator.regexp("ab", '^ab$'), null); + assertEquals(angular.validator.regexp("ab", '^axb$', "E3"), "E3"); +}; + +ValidatorTest.prototype.testNumber = function() { + assertEquals(angular.validator.number("ab"), "Not a number"); + assertEquals(angular.validator.number("-0.1",0), "Value can not be less than 0."); + assertEquals(angular.validator.number("10.1",0,10), "Value can not be greater than 10."); + assertEquals(angular.validator.number("1.2"), null); + assertEquals(angular.validator.number(" 1 ", 1, 1), null); +}; + +ValidatorTest.prototype.testInteger = function() { + assertEquals(angular.validator.integer("ab"), "Not a number"); + assertEquals(angular.validator.integer("1.1"), "Not a whole number"); + assertEquals(angular.validator.integer("1.0"), "Not a whole number"); + assertEquals(angular.validator.integer("1."), "Not a whole number"); + assertEquals(angular.validator.integer("-1",0), "Value can not be less than 0."); + assertEquals(angular.validator.integer("11",0,10), "Value can not be greater than 10."); + assertEquals(angular.validator.integer("1"), null); + assertEquals(angular.validator.integer(" 1 ", 1, 1), null); +}; + +ValidatorTest.prototype.testDate = function() { + var error = "Value is not a date. (Expecting format: 12/31/2009)."; + assertEquals(angular.validator.date("ab"), error); + assertEquals(angular.validator.date("12/31/2009"), null); +}; + +ValidatorTest.prototype.testPhone = function() { + var error = "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; + assertEquals(angular.validator.phone("ab"), error); + assertEquals(null, angular.validator.phone("1(408)757-3023")); + assertEquals(null, angular.validator.phone("+421 (0905) 933 297")); + assertEquals(null, angular.validator.phone("+421 0905 933 297")); +}; + +ValidatorTest.prototype.testSSN = function() { + var error = "SSN needs to be in 999-99-9999 format."; + assertEquals(angular.validator.ssn("ab"), error); + assertEquals(angular.validator.ssn("123-45-6789"), null); +}; + +ValidatorTest.prototype.testURL = function() { + var error = "URL needs to be in http://server[:port]/path format."; + assertEquals(angular.validator.url("ab"), error); + assertEquals(angular.validator.url("http://server:123/path"), null); +}; + +ValidatorTest.prototype.testEmail = function() { + var error = "Email needs to be in username@host.com format."; + assertEquals(error, angular.validator.email("ab")); + assertEquals(null, angular.validator.email("misko@hevery.com")); +}; + +ValidatorTest.prototype.testJson = function() { + assertNotNull(angular.validator.json("'")); + assertNotNull(angular.validator.json("''X")); + assertNull(angular.validator.json("{}")); +}; + +describe('Validator:asynchronous', function(){ + var asynchronous = angular.validator.asynchronous; + var self; + var value, fn; + + beforeEach(function(){ + var invalidWidgets = angularService('$invalidWidgets')(); + value = null; + fn = null; + self = { + $element:jqLite(''), + $invalidWidgets:invalidWidgets, + $eval: noop + }; + self.$element.data('$validate', noop); + self.$root = self; + }); + + afterEach(function(){ + if (self.$element) self.$element.remove(); + var oldCache = jqCache; + jqCache = {}; + expect(size(oldCache)).toEqual(0); + }); + + it('should make a request and show spinner', function(){ + var value, fn; + var scope = compile(''); + scope.$init(); + var input = scope.$element; + scope.asyncFn = function(v,f){ + value=v; fn=f; + }; + scope.name = "misko"; + scope.$eval(); + expect(value).toEqual('misko'); + expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy(); + fn("myError"); + expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy(); + expect(input.attr(NG_VALIDATION_ERROR)).toEqual("myError"); + scope.$element.remove(); + }); + + it("should not make second request to same value", function(){ + asynchronous.call(self, "kai", function(v,f){value=v; fn=f;}); + expect(value).toEqual('kai'); + expect(self.$invalidWidgets[0]).toEqual(self.$element); + + var spy = jasmine.createSpy(); + asynchronous.call(self, "kai", spy); + expect(spy).wasNotCalled(); + + asynchronous.call(self, "misko", spy); + expect(spy).wasCalled(); + }); + + it("should ignore old callbacks, and not remove spinner", function(){ + var firstCb, secondCb; + asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;}); + asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;}); + + firstCb(); + expect(self.$element.hasClass('ng-input-indicator-wait')).toBeTruthy(); + + secondCb(); + expect(self.$element.hasClass('ng-input-indicator-wait')).toBeFalsy(); + }); + + it("should handle update function", function(){ + var scope = angular.compile(''); + scope.asyncFn = jasmine.createSpy(); + scope.updateFn = jasmine.createSpy(); + scope.name = 'misko'; + scope.$init(); + scope.$eval(); + expect(scope.asyncFn).wasCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]); + assertTrue(scope.$element.hasClass('ng-input-indicator-wait')); + scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'}); + assertFalse(scope.$element.hasClass('ng-input-indicator-wait')); + assertEquals('myError', scope.$element.attr('ng-validation-error')); + expect(scope.updateFn.mostRecentCall.args[0]).toEqual({id: 1234, data:'data'}); + scope.$element.remove(); + }); + +}); diff --git a/test/angular-mocks.js b/test/angular-mocks.js new file mode 100644 index 00000000..8838b2cd --- /dev/null +++ b/test/angular-mocks.js @@ -0,0 +1,101 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +function MockBrowser() { + var self = this, + expectations = {}, + requests = []; + this.isMock = true; + self.url = "http://server"; + self.watches = []; + + self.xhr = function(method, url, data, callback) { + if (angular.isFunction(data)) { + callback = data; + data = null; + } + if (data && angular.isObject(data)) data = angular.toJson(data); + if (data && angular.isString(data)) url += "|" + data; + var expect = expectations[method] || {}; + var response = expect[url]; + if (!response) { + throw "Unexepected request for method '" + method + "' and url '" + url + "'."; + } + requests.push(function(){ + callback(response.code, response.response); + }); + }; + self.xhr.expectations = expectations; + self.xhr.requests = requests; + self.xhr.expect = function(method, url, data) { + if (data && angular.isObject(data)) data = angular.toJson(data); + if (data && angular.isString(data)) url += "|" + data; + var expect = expectations[method] || (expectations[method] = {}); + return { + respond: function(code, response) { + if (!angular.isNumber(code)) { + response = code; + code = 200; + } + expect[url] = {code:code, response:response}; + } + }; + }; + self.xhr.expectGET = angular.bind(self, self.xhr.expect, 'GET'); + self.xhr.expectPOST = angular.bind(self, self.xhr.expect, 'POST'); + self.xhr.expectDELETE = angular.bind(self, self.xhr.expect, 'DELETE'); + self.xhr.expectPUT = angular.bind(self, self.xhr.expect, 'PUT'); + self.xhr.flush = function() { + while(requests.length) { + requests.pop()(); + } + }; +} +MockBrowser.prototype = { + + hover: function(onHover) { + }, + + getUrl: function(){ + return this.url; + }, + + setUrl: function(url){ + this.url = url; + }, + + watchUrl: function(fn) { + this.watches.push(fn); + }, + + fireUrlWatchers: function() { + for(var i=0; i"); + form.data('scope', scope); + var c = form.find('c'); + assertTrue(scope === c.scope()); +}; + +ScopeTest.prototype.testGetScopeRetrievalIntermediateNode = function(){ + var scope = {}; + var form = jQuery(""); + form.find("b").data('scope', scope); + var b = form.find('b'); + assertTrue(scope === b.scope()); +}; + +ScopeTest.prototype.testNoScopeDoesNotCauseInfiniteRecursion = function(){ + var form = jQuery(""); + var c = form.find('c'); + assertTrue(!c.scope()); +}; + +ScopeTest.prototype.testScopeEval = function(){ + var scope = new Scope({b:345}); + assertEquals(scope.eval('b = 123'), 123); + assertEquals(scope.get('b'), 123); +}; + +ScopeTest.prototype.testScopeFromPrototype = function(){ + var scope = new Scope({b:123}); + scope.eval('a = b'); + scope.eval('b = 456'); + assertEquals(scope.get('a'), 123); + assertEquals(scope.get('b'), 456); +}; + +ScopeTest.prototype.testSetScopeGet = function(){ + var scope = new Scope(); + assertEquals(987, scope.set('a', 987)); + assertEquals(scope.get('a'), 987); + assertEquals(scope.eval('a'), 987); +}; + +ScopeTest.prototype.testGetChain = function(){ + var scope = new Scope({a:{b:987}}); + assertEquals(scope.get('a.b'), 987); + assertEquals(scope.eval('a.b'), 987); +}; + +ScopeTest.prototype.testGetUndefinedChain = function(){ + var scope = new Scope(); + assertEquals(typeof scope.get('a.b'), 'undefined'); +}; + +ScopeTest.prototype.testSetChain = function(){ + var scope = new Scope({a:{}}); + scope.set('a.b', 987); + assertEquals(scope.get('a.b'), 987); + assertEquals(scope.eval('a.b'), 987); +}; + +ScopeTest.prototype.testSetGetOnChain = function(){ + var scope = new Scope(); + scope.set('a.b', 987); + assertEquals(scope.get('a.b'), 987); + assertEquals(scope.eval('a.b'), 987); +}; + +ScopeTest.prototype.testGlobalFunctionAccess =function(){ + window['scopeAddTest'] = function (a, b) {return a+b;}; + var scope = new Scope({window:window}); + assertEquals(scope.eval('window.scopeAddTest(1,2)'), 3); + + scope.set('add', function (a, b) {return a+b;}); + assertEquals(scope.eval('add(1,2)'), 3); + + scope.set('math.add', function (a, b) {return a+b;}); + assertEquals(scope.eval('math.add(1,2)'), 3); +}; + +ScopeTest.prototype.testValidationEval = function(){ + expectAsserts(4); + var scope = new Scope(); + scope.set("name", "misko"); + angular.validator.testValidator = function(value, expect){ + assertEquals("misko", this.name); + return value == expect ? null : "Error text"; + }; + + assertEquals("Error text", scope.validate("testValidator:'abc'", 'x')); + assertEquals(null, scope.validate("testValidator:'abc'", 'abc')); + + delete angular.validator['testValidator']; +}; + +ScopeTest.prototype.testCallingNonExistantMethodShouldProduceFriendlyException = function() { + expectAsserts(1); + var scope = new Scope({obj:{}}); + try { + scope.eval("obj.iDontExist()"); + fail(); + } catch (e) { + assertEquals("Expression 'obj.iDontExist' is not a function.", e); + } +}; + +ScopeTest.prototype.testAccessingWithInvalidPathShouldThrowError = function() { + var scope = new Scope(); + try { + scope.get('a.{{b}}'); + fail(); + } catch (e) { + assertEquals("Expression 'a.{{b}}' is not a valid expression for accesing variables.", e); + } +}; + +ScopeTest.prototype.testItShouldHave$parent = function() { + var parent = new Scope({}, "ROOT"); + var child = new Scope(parent.state); + assertSame("parent", child.state.$parent, parent.state); + assertSame("root", child.state.$root, parent.state); +}; + +ScopeTest.prototype.testItShouldHave$root = function() { + var scope = new Scope({}, "ROOT"); + assertSame(scope.state.$root, scope.state); +}; + +ScopeTest.prototype.testItShouldBuildPathOnUndefined = function(){ + var scope = new Scope({}, "ROOT"); + scope.setEval("a.$b.c", 1); + assertJsonEquals({$b:{c:1}}, scope.get("a")); +}; + +ScopeTest.prototype.testItShouldMapUnderscoreFunctions = function(){ + var scope = new Scope({}, "ROOT"); + scope.set("a", [1,2,3]); + assertEquals('function', typeof scope.get("a.$size")); + scope.eval("a.$includeIf(4,true)"); + assertEquals(4, scope.get("a.$size")()); + assertEquals(4, scope.eval("a.$size()")); + assertEquals('undefined', typeof scope.get("a.dontExist")); +}; diff --git a/test/delete/WidgetsTest.js b/test/delete/WidgetsTest.js new file mode 100644 index 00000000..313d7372 --- /dev/null +++ b/test/delete/WidgetsTest.js @@ -0,0 +1,268 @@ +WidgetTest = TestCase('WidgetTest'); + +WidgetTest.prototype.testRequired = function () { + var view = $(''); + var scope = new Scope({$invalidWidgets:[]}); + var cntl = new TextController(view[0], 'a', angularFormatter.noop); + cntl.updateView(scope); + assertTrue(view.hasClass('ng-validation-error')); + assertEquals("Required Value", view.attr('ng-error')); + scope.set('a', 'A'); + cntl.updateView(scope); + assertFalse(view.hasClass('ng-validation-error')); + assertEquals("undefined", typeof view.attr('ng-error')); +}; + +WidgetTest.prototype.testValidator = function () { + var view = $(''); + var scope = new Scope({$invalidWidgets:[]}); + var cntl = new TextController(view[0], 'a', angularFormatter.noop); + angular.validator.testValidator = function(value, expect){ + return value == expect ? false : "Error text"; + }; + + scope.set('a', ''); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), false); + assertEquals(null, view.attr('ng-error')); + + scope.set('a', 'X'); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), true); + assertEquals(view.attr('ng-error'), "Error text"); + assertEquals("Error text", view.attr('ng-error')); + + scope.set('a', 'ABC'); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), false); + assertEquals(view.attr('ng-error'), null); + assertEquals(null, view.attr('ng-error')); + + delete angular.validator['testValidator']; +}; + +WidgetTest.prototype.testRequiredValidator = function () { + var view = $(''); + var scope = new Scope({$invalidWidgets:[]}); + var cntl = new TextController(view[0], 'a', angularFormatter.noop); + angular.validator.testValidator = function(value, expect){ + return value == expect ? null : "Error text"; + }; + + scope.set('a', ''); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), true); + assertEquals("Required Value", view.attr('ng-error')); + + scope.set('a', 'X'); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), true); + assertEquals("Error text", view.attr('ng-error')); + + scope.set('a', 'ABC'); + cntl.updateView(scope); + assertEquals(view.hasClass('ng-validation-error'), false); + assertEquals(null, view.attr('ng-error')); + + delete angular.validator['testValidator']; +}; + +TextControllerTest = TestCase("TextControllerTest"); + +TextControllerTest.prototype.testDatePicker = function() { + var input = $(''); + input.data('scope', new Scope()); + var body = $(document.body); + body.append(input); + var binder = new Binder(input[0], new WidgetFactory()); + assertTrue('before', input.data('datepicker') === undefined); + binder.compile(); + assertTrue('after', input.data('datepicker') !== null); + assertTrue(body.html(), input.hasClass('hasDatepicker')); +}; + +RepeaterUpdaterTest = TestCase("RepeaterUpdaterTest"); + +RepeaterUpdaterTest.prototype.testRemoveThenAdd = function() { + var view = $("
                "); + var template = function () { + return $("
              • "); + }; + var repeater = new RepeaterUpdater(view.find("span"), "a in b", template, ""); + var scope = new Scope(); + scope.set('b', [1,2]); + + repeater.updateView(scope); + + scope.set('b', []); + repeater.updateView(scope); + + scope.set('b', [1]); + repeater.updateView(scope); + assertEquals(1, view.find("li").size()); +}; + +RepeaterUpdaterTest.prototype.testShouldBindWidgetOnRepeaterClone = function(){ + //fail(); +}; + +RepeaterUpdaterTest.prototype.testShouldThrowInformativeSyntaxError= function(){ + expectAsserts(1); + try { + var repeater = new RepeaterUpdater(null, "a=b"); + } catch (e) { + assertEquals("Expected ng-repeat in form of 'item in collection' but got 'a=b'.", e); + } +}; + +SelectControllerTest = TestCase("SelectControllerTest"); + +SelectControllerTest.prototype.testShouldUpdateModelNullOnNothingSelected = function(){ + var scope = new Scope(); + var view = {selectedIndex:-1, options:[]}; + var cntl = new SelectController(view, 'abc'); + cntl.updateModel(scope); + assertNull(scope.get('abc')); +}; + +SelectControllerTest.prototype.testShouldUpdateModelWhenNothingSelected = function(){ + var scope = new Scope(); + var view = {value:'123'}; + var cntl = new SelectController(view, 'abc'); + cntl.updateView(scope); + assertEquals("123", scope.get('abc')); +}; + +BindUpdaterTest = TestCase("BindUpdaterTest"); + +BindUpdaterTest.prototype.testShouldDisplayNothingForUndefined = function () { + var view = $(''); + var controller = new BindUpdater(view[0], "{{a}}"); + var scope = new Scope(); + + scope.set('a', undefined); + controller.updateView(scope); + assertEquals("", view.text()); + + scope.set('a', null); + controller.updateView(scope); + assertEquals("", view.text()); +}; + +BindUpdaterTest.prototype.testShouldDisplayJsonForNonStrings = function () { + var view = $(''); + var controller = new BindUpdater(view[0], "{{obj}}"); + + controller.updateView(new Scope({obj:[]})); + assertEquals("[]", view.text()); + + controller.updateView(new Scope({obj:{text:'abc'}})); + assertEquals('abc', fromJson(view.text()).text); +}; + + +BindUpdaterTest.prototype.testShouldInsertHtmlNode = function () { + var view = $(''); + var controller = new BindUpdater(view[0], "&{{obj}}"); + var scope = new Scope(); + + scope.set("obj", $('
                myDiv
                ')[0]); + controller.updateView(scope); + assertEquals("&myDiv", view.text()); +}; + + +BindUpdaterTest.prototype.testShouldDisplayTextMethod = function () { + var view = $('
                '); + var controller = new BindUpdater(view[0], "{{obj}}"); + var scope = new Scope(); + + scope.set("obj", new angular.filter.Meta({text:function(){return "abc";}})); + controller.updateView(scope); + assertEquals("abc", view.text()); + + scope.set("obj", new angular.filter.Meta({text:"123"})); + controller.updateView(scope); + assertEquals("123", view.text()); + + scope.set("obj", {text:"123"}); + controller.updateView(scope); + assertEquals("123", fromJson(view.text()).text); +}; + +BindUpdaterTest.prototype.testShouldDisplayHtmlMethod = function () { + var view = $('
                '); + var controller = new BindUpdater(view[0], "{{obj}}"); + var scope = new Scope(); + + scope.set("obj", new angular.filter.Meta({html:function(){return "a
                b
                c";}})); + controller.updateView(scope); + assertEquals("abc", view.text()); + + scope.set("obj", new angular.filter.Meta({html:"1
                2
                3"})); + controller.updateView(scope); + assertEquals("123", view.text()); + + scope.set("obj", {html:"123"}); + controller.updateView(scope); + assertEquals("123", fromJson(view.text()).html); +}; + +BindUpdaterTest.prototype.testUdateBoolean = function() { + var view = $('
                '); + var controller = new BindUpdater(view[0], "{{true}}, {{false}}"); + controller.updateView(new Scope()); + assertEquals('true, false', view.text()); +}; + +BindAttrUpdaterTest = TestCase("BindAttrUpdaterTest"); + +BindAttrUpdaterTest.prototype.testShouldLoadBlankImageWhenBindingIsUndefined = function () { + var view = $(''); + var controller = new BindAttrUpdater(view[0], {src: '{{imageUrl}}'}); + + var scope = new Scope(); + scope.set('imageUrl', undefined); + scope.set('$config.blankImage', 'http://server/blank.gif'); + + controller.updateView(scope); + assertEquals("http://server/blank.gif", view.attr('src')); +}; + +RepeaterUpdaterTest.prototype.testShouldNotDieWhenRepeatExpressionIsNull = function() { + var rep = new RepeaterUpdater(null, "$item in items", null, null); + var scope = new Scope(); + scope.set('items', undefined); + rep.updateView(scope); +}; + +RepeaterUpdaterTest.prototype.testShouldIterateOverKeys = function() { + var rep = new RepeaterUpdater(null, "($k,_v) in items", null, null); + assertEquals("items", rep.iteratorExp); + assertEquals("_v", rep.valueExp); + assertEquals("$k", rep.keyExp); +}; + +EvalUpdaterTest = TestCase("EvalUpdaterTest"); +EvalUpdaterTest.prototype.testEvalThrowsException = function(){ + var view = $('
                '); + var eval = new EvalUpdater(view[0], 'undefined()'); + + eval.updateView(new Scope()); + assertTrue(!!view.attr('ng-error')); + assertTrue(view.hasClass('ng-exception')); + + eval.exp = "1"; + eval.updateView(new Scope()); + assertFalse(!!view.attr('ng-error')); + assertFalse(view.hasClass('ng-exception')); +}; + +RadioControllerTest = TestCase("RadioController"); +RadioControllerTest.prototype.testItShouldTreatTrueStringAsBoolean = function () { + var view = $(''); + var radio = new RadioController(view[0], 'select'); + var scope = new Scope({select:true}); + radio.updateView(scope); + assertTrue(view[0].checked); +}; diff --git a/test/directivesSpec.js b/test/directivesSpec.js new file mode 100644 index 00000000..42869a05 --- /dev/null +++ b/test/directivesSpec.js @@ -0,0 +1,226 @@ +describe("directives", function(){ + + var compile, model, element; + + beforeEach(function() { + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); + compile = function(html) { + element = jqLite(html); + model = compiler.compile(element)(element); + model.$init(); + return model; + }; + }); + + afterEach(function() { + if (model && model.$element) model.$element.remove(); + expect(size(jqCache)).toEqual(0); + }); + + it("should ng-init", function() { + var scope = compile('
                '); + expect(scope.a).toEqual(123); + }); + + it("should ng-eval", function() { + var scope = compile('
                '); + expect(scope.a).toEqual(1); + scope.$eval(); + expect(scope.a).toEqual(2); + }); + + it('should ng-bind', function() { + var scope = compile('
                '); + expect(element.text()).toEqual(''); + scope.a = 'misko'; + scope.$eval(); + expect(element.text()).toEqual('misko'); + }); + + it('should ng-bind html', function() { + var scope = compile('
                '); + scope.html = '
                hello
                '; + scope.$eval(); + expect(lowercase(element.html())).toEqual('
                hello
                '); + }); + + it('should ng-bind element', function() { + angularFilter.myElement = function() { + return jqLite('hello'); + }; + var scope = compile('
                '); + scope.$eval(); + expect(lowercase(element.html())).toEqual('hello'); + }); + + it('should ng-bind-template', function() { + var scope = compile('
                '); + scope.$set('name', 'Misko'); + scope.$eval(); + expect(element.text()).toEqual('Hello Misko!'); + }); + + it('should ng-bind-attr', function(){ + var scope = compile(''); + expect(element.attr('src')).toEqual('http://localhost/mysrc'); + expect(element.attr('alt')).toEqual('myalt'); + }); + + it('should remove special attributes on false', function(){ + var scope = compile(''); + var input = scope.$element[0]; + expect(input.disabled).toEqual(false); + expect(input.readOnly).toEqual(false); + expect(input.checked).toEqual(false); + + scope.disabled = true; + scope.readonly = true; + scope.checked = true; + scope.$eval(); + + expect(input.disabled).toEqual(true); + expect(input.readOnly).toEqual(true); + expect(input.checked).toEqual(true); + }); + + it('should ng-non-bindable', function(){ + var scope = compile('
                '); + scope.$set('name', 'misko'); + scope.$eval(); + expect(element.text()).toEqual(''); + }); + + it('should ng-repeat over array', function(){ + var scope = compile('
                '); + + scope.$set('items', ['misko', 'shyam']); + scope.$eval(); + expect(element.text()).toEqual('misko;shyam;'); + + scope.$set('items', ['adam', 'kai', 'brad']); + scope.$eval(); + expect(element.text()).toEqual('adam;kai;brad;'); + + scope.$set('items', ['brad']); + scope.$eval(); + expect(element.text()).toEqual('brad;'); + }); + + it('should ng-repeat over object', function(){ + var scope = compile('
                '); + scope.$set('items', {misko:'swe', shyam:'set'}); + scope.$eval(); + expect(element.text()).toEqual('misko:swe;shyam:set;'); + }); + + it('should set ng-repeat to [] if undefinde', function(){ + var scope = compile('
                '); + expect(scope.items).toEqual([]); + }); + + it('should error on wrong parsing of ng-repeat', function(){ + var scope = compile('
                '); + var log = ""; + log += element.attr('ng-exception') + ';'; + log += element.hasClass('ng-exception') + ';'; + expect(log).toEqual("\"Expected ng-repeat in form of 'item in collection' but got 'i dont parse'.\";true;"); + }); + + it('should ng-watch', function(){ + var scope = compile('
                '); + scope.$eval(); + scope.$eval(); + expect(scope.$get('count')).toEqual(0); + + scope.$set('i', 0); + scope.$eval(); + scope.$eval(); + expect(scope.$get('count')).toEqual(1); + }); + + it('should ng-click', function(){ + var scope = compile('
                '); + scope.$eval(); + expect(scope.$get('clicked')).toBeFalsy(); + + element.trigger('click'); + expect(scope.$get('clicked')).toEqual(true); + }); + + it('should ng-class', function(){ + var scope = compile('
                '); + scope.$eval(); + expect(element.hasClass('existing')).toBeTruthy(); + expect(element.hasClass('A')).toBeTruthy(); + expect(element.hasClass('B')).toBeTruthy(); + }); + + it('should ng-class odd/even', function(){ + var scope = compile('