Compare commits

...

153 Commits

Author SHA1 Message Date
Igor Minar 9b53b25f15 cutting the 0.9.4 total-recall release 2010-11-18 22:40:01 -08:00
Igor Minar 3fbfa357ca updated release notes for the 0.9.4 total-recall release 2010-11-18 22:40:00 -08:00
Igor Minar 50ef1f8e35 don't escape $ in hashpath either 2010-11-18 22:40:00 -08:00
Igor Minar 66c0bfaa8e don't escape ! and : in hashPath
This is a temporary fix for Issue #158
2010-11-18 20:51:31 -08:00
Igor Minar 1719b0aca5 fix all closure compilation warnings due to invalid function types 2010-11-18 17:03:43 -08:00
Igor Minar 7ee102eecf add a feedback link to the doc pages 2010-11-18 16:53:11 -08:00
Igor Minar fc7f11d03b add @workInProgress tag and mark all @ngdocs as work in progress 2010-11-18 16:28:42 -08:00
Igor Minar 3c7874b07b don't encode page.name (hash) in docs.js
see discussion in https://github.com/angular/angular.js/pull/158"
2010-11-18 11:56:10 -08:00
Igor Minar 7f339a1782 escape code in ng:bind-attr jsdoc 2010-11-18 11:33:09 -08:00
Igor Minar 72a5f007d8 most of the documentation for angular.scope and friends 2010-11-18 02:35:30 -08:00
Igor Minar 63380bbbda title for index.html should not show raw binding while the app bootstraps 2010-11-18 02:35:30 -08:00
Igor Minar c635b69f5c fix docs and examples for ng:format, ng:required and ng:validate 2010-11-18 02:35:30 -08:00
Igor Minar 522ec1a9ec move attribute widgets to widgets.js file
- move @ng:repeat to widgets.js and its specs to widgetsSpecs.js
- move @ng:non-bindable to widgets.js and its specs to widgetsSpecs.js
- make widget.template suitable for attribute widgets
- fix up the js docs for attribute widgets
2010-11-18 02:35:29 -08:00
Igor Minar 9cb57772a4 fix docs for angular.directive and ng:autobind 2010-11-18 02:35:29 -08:00
Igor Minar d54f09ef29 add spec for incrementing headings 2010-11-18 02:35:29 -08:00
Igor Minar 65989c6f0d add support for {@link} tags within @description and remove implicit linking
use as:
- foo {@link bar}
- foo {@link bar desc}

I'm removing implicit linking because it unintentionally links stuff and
generally interferes with other conversions. We have to link stuff explicitely
from now on.
2010-11-18 02:35:29 -08:00
Igor Minar 4491bbdede docs linkifying regexp should not mess up links alreaded converete by markdown 2010-11-18 02:35:28 -08:00
Igor Minar a6978b201b make @param type and description non-optional 2010-11-18 02:35:28 -08:00
Igor Minar 28e72cbe6b CSS, Parameters, Returns template changes
- make css section optional
- make returns section optional
- change format of the parameters section
- properly format the Returns section
2010-11-18 02:34:55 -08:00
Igor Minar 916dadd8ec adjust spacing of headings in the main doc div 2010-11-18 02:34:54 -08:00
Igor Minar e509ec37f5 fixing angular.lowercase and angular.upppercase jsdocs + api 2010-11-18 02:34:54 -08:00
Igor Minar ee0e9a4452 adding support for @param.optional 2010-11-18 02:34:54 -08:00
Igor Minar 9d36368ff9 fixing angular.filter.number jsdocs 2010-11-18 02:34:54 -08:00
Igor Minar d4bcee0799 toJson and fromJson jsdocs 2010-11-18 02:34:54 -08:00
Igor Minar dd687e2bf5 @returns description should support markdown 2010-11-18 02:34:53 -08:00
Igor Minar 4c69d694d7 make @returns type non-optional 2010-11-18 02:34:53 -08:00
Igor Minar ff7c738c21 fix ng docs for angular, angular.lowercase and uppercase 2010-11-18 02:34:53 -08:00
Igor Minar 51a22cf435 group utility methods/objects while sorting stuff for the side bar 2010-11-18 02:34:53 -08:00
Igor Minar c2c60ab49a remove extra returns tag 2010-11-18 02:34:53 -08:00
Igor Minar 71c2f24fc6 remove extra toolbar directive and support multiple pre's in describe 2010-11-18 02:34:52 -08:00
Igor Minar fc78738cc6 scope docs + lowercase doc fix 2010-11-18 02:34:52 -08:00
Igor Minar c7052f098d add support for @deprecated ng:doc annotation + show warnings in templates 2010-11-18 02:34:31 -08:00
Igor Minar 7d6f5f986e add function.template 2010-11-18 02:34:30 -08:00
Igor Minar beeb5ff908 fix regexp for @param parsing in ng:docs
There is an extra + in the regexp which causes exponential increase in time
needed to parse a @param annotation when the length of default value increases
linearly.
2010-11-16 14:44:35 -08:00
Misko Hevery b2d63ac48b Changed error handling so that better stack traces are displayed in the ng-errors 2010-11-16 14:19:55 -08:00
Igor Minar 4af32de84a docs should use ng:include onload 2010-11-16 13:45:45 -08:00
Igor Minar a130bb899d add onload attribute to ng:include 2010-11-16 11:35:43 -08:00
Vojta Jina cc749760fd Added basic Services, which support @memberOf and @methodOf 2010-11-15 21:55:37 -08:00
Misko Hevery b467a50bc7 generate keyword for searches; improved layout of doc 2010-11-15 12:28:08 -08:00
Misko Hevery a1652057a5 changed to ! notation for indexable apps 2010-11-15 10:04:17 -08:00
Misko Hevery 7e6f999221 added remaining directives and search box. 2010-11-15 10:04:17 -08:00
Igor Minar 625cc7609c fix code name in the change log 2010-11-12 16:18:11 -08:00
Igor Minar c51273b1fb Add test coverage analysis.
- jstd upgrade to head from 2010-11-11
- coverage plugin from the same head as jstd
- test-coverage.sh and server-coverage.sh scripts
- jstd configuration

Generate html by installing lconv (brew or port) and run:

genhtml tmp/lcov/jsTestDriver.conf-coverage.dat

to generate html docs.
2010-11-12 16:13:53 -08:00
Igor Minar 0a8b3161b1 $watch should optionally skip listener exec
- if initRun param is set to false, listener doesn't execute
- the oldValue should equal newValue during the initial execution
- added docs
- added specs
2010-11-11 16:39:01 -08:00
Igor Minar ba554eeb1b preparations for 0.9.4 total-recall iteration 2010-11-11 16:38:23 -08:00
Igor Minar 5f0af2cd0e cutting the 0.9.3 cold-resistance release 2010-11-10 22:15:16 -08:00
Igor Minar 7411b24812 updating the release notes for 0.9.3 release 2010-11-10 22:14:16 -08:00
Igor Minar ae5f6f48b4 scenario repeater should match only visible stuff 2010-11-10 21:04:44 -08:00
Igor Minar c5b2bf083c Make ng:repeat expose $position.
- $position is a textual representation of the position of
  repeated item ('first', 'middle', 'last')
- added specs for $index
2010-11-10 21:04:44 -08:00
Misko Hevery 0499c47270 added ng:switch-when-default; changed $watch to always fire on init. (may be backward incompatible) 2010-11-10 21:01:17 -08:00
Misko Hevery 43a4ff4cdf Example snippets now have full html snippets 2010-11-10 21:01:05 -08:00
Misko Hevery 6b8ed42670 Added Directives 2010-11-10 12:02:49 -08:00
Elliott Sprehn c57df3dc77 Make the docs look a little nicer 2010-11-10 11:01:30 -08:00
Igor Minar 6d53808475 improve jsdocs
- improve json filter example
- improve filter overview doc
- improving validator overview jsdocs
- simplify number filter examples and make them live + add specs
- various doc fixes
2010-11-09 22:33:27 -08:00
Misko Hevery a7e8a503fd proper trimming of leading whitespace; url restriction to angular.* 2010-11-09 22:27:37 -08:00
Igor Minar 324694a58b Better example widget
- syntax highlighting
- tabless design
- rename widget to doc:example
- rename widget files (wiki_widget.* -> doc_widget.*)
- example section is now optional
2010-11-09 22:27:31 -08:00
Igor Minar effcd340e9 updating watchr-docs.rb 2010-11-09 21:44:51 -08:00
Igor Minar 264f960800 added spec for auto bootstrap with #autobind 2010-11-09 19:58:42 -08:00
Elliott Sprehn 257e97a65f Support substring matching of bindings with repeater(). Closes #123 2010-11-09 13:39:12 -08:00
Misko Hevery c048f0d8e8 Added formatter documentation. 2010-11-09 09:15:29 -08:00
Elliott Sprehn 96e37a0866 Add key/value jQuery methods in a loop. Support 'css' method.
Closes #134
2010-11-09 08:02:49 -08:00
Vojta Jina 5062d32621 Updated parser tests to use jasmine expects
And some formatting as well and a bit of rafactor...
2010-11-09 07:38:03 -08:00
Vojta Jina d458f31711 Updated filters tests to use jasmine expects
And some small formatting...
2010-11-09 07:38:03 -08:00
Igor Minar fc9ce9ec07 make angular.String.toDate consider all time fractions as optional 2010-11-08 22:49:30 -08:00
Igor Minar da17c61444 angular.Date.toString should use toISOString if available 2010-11-08 22:49:30 -08:00
Igor Minar e5c135ac50 Support ISO 8601 extended datetime format troughout angular.
Support ISO 8601 extended format datetime strings (YYYY-MM-DDTHH:mm:ss.SSSZ) as defined
  in EcmaScript 5 throughout angular. This means that the following apis switched from
  YYYY-MM-DDTHH:mm:ssZ to YYYY-MM-DDTHH:mm:ss.SSSZ (note the added millis) when representing dates:
  - angular.Date.toString
  - angular.String.toDate
  - JSON serialization and deserialization (used by json filter, $xhr and $resource)
2010-11-08 22:49:30 -08:00
Igor Minar 1a43f36e23 Add isDate method + fix old code 2010-11-08 22:49:29 -08:00
Igor Minar 1c305dc67a fixing spec description for the date filter 2010-11-08 22:46:41 -08:00
Igor Minar a397645537 date filter should accept ISO 8601 formatted string as input
Closes #125
2010-11-08 22:46:41 -08:00
Igor Minar f077649f48 TzDate should support various UTC methods 2010-11-08 22:46:41 -08:00
Vojta Jina f3ac2cd434 Update AngularSpec tests to use BDD style - jasmine expect 2010-11-07 14:42:04 -08:00
Vojta Jina 7779630989 Added tests for angular.service
- should allow to override a service
- should preserve angular properties on override
- should not preserve non-angular properties on override
2010-11-07 14:42:03 -08:00
Vojta Jina 00ca67e4be Issue #51: Update extensionMap()
If user override existing extension, angular properties ($) will be preserved.

This piece of logic could be refactored into separate method:
Something like we have extend(), addMissingProperties() - I can't find a name
for this method...

Closes #51
2010-11-07 14:42:03 -08:00
Misko Hevery 91b6c5f7ff Added documentation for validators.
BACKWARD INCOMPATIBLE: removed ssn validators, since it is unlikely that most people will need it and if they do, they can added it thorough RegExp
2010-11-07 13:06:55 -08:00
Vojta Jina 5be325a0c1 Refactored toJsonArray(), added isBoolean() function 2010-11-05 21:39:00 -07:00
Vojta Jina b7027b9d87 Updated toJson() to not serialize window/document objects.
The reason to void these to objects is that they cause all sorts
of problems like exceptions being thrown and infinite loops occuring
when we iterate over object properties.
2010-11-05 21:39:00 -07:00
Vojta Jina fe8353bc5e Changed toJson() to not ignore $ properties 2010-11-05 21:39:00 -07:00
Vojta Jina c780030c6e Json - corrected test descriptions and changed to BDD style expect() 2010-11-05 21:39:00 -07:00
Misko Hevery d5e9f38f3d fix error in json parser which did not allow 1.2E10 as number 2010-11-05 16:41:36 -07:00
Misko Hevery dc66687149 added tests for documentation 2010-11-05 15:05:24 -07:00
Misko Hevery 3d6a099d6e changed to showdown from markup. added validator overview 2010-11-05 13:32:37 -07:00
Misko Hevery 8767e766d1 eclipse settings for auto run of docs 2010-11-04 14:25:11 -07:00
Misko Hevery 47066e70e1 added documentation for ng:include and ng:widget and test for doc collector. 2010-11-04 14:24:31 -07:00
Misko Hevery c0d30aedfc added LICENSE to IDEA 2010-11-04 11:07:49 -07:00
Misko Hevery b246d6e2ab rename Index.js to index.js 2010-11-04 11:02:45 -07:00
Igor Minar 3b04b48b7c preparations for the 0.9.3 cold-resistance iteration 2010-11-03 13:09:52 -07:00
Igor Minar 0462422089 cutting the 0.9.2 faunal-mimicry release 2010-11-03 13:06:45 -07:00
Igor Minar bb460d6355 updating release notes 2010-11-03 13:05:12 -07:00
Igor Minar bbb19a4d16 fixing docs scenario runner template 2010-11-03 12:55:17 -07:00
Elliott Sprehn ee5e881b9e Fix JSON serialization breakage in WebKit browsers 2010-11-03 11:47:06 -07:00
Elliott Sprehn 690dfe000b Serialize RegExp to string in JSON. Closes #119. 2010-11-03 11:10:51 -07:00
Igor Minar 91c835dc0e fix dslSpec for IE 2010-11-03 10:41:37 -07:00
Igor Minar dfa8baf59a hide example for angular.filter 2010-11-03 10:02:20 -07:00
Igor Minar a8efd43d25 doc parser should recognize the 'defaults' syntax for params 2010-11-03 09:47:23 -07:00
Igor Minar 96abeb1074 adding watchr-docs.rb 2010-11-03 09:47:22 -07:00
Misko Hevery 5159eb7635 fix the linky filter 2010-11-03 09:47:22 -07:00
Igor Minar 1ba8c2a33a more documentation for filters 2010-11-03 09:47:22 -07:00
Misko Hevery 659af29adb jsdoc parser + generator + viewer + scenario runner
- parse jsdocs from source code
- generate prerendered (markdown + mustache) partials
- generate json
- generate scenario runner for examples in docs
- basic angular doc viewer
2010-11-03 09:47:22 -07:00
Igor Minar 1fe7e3a130 add jsdocs for angular and filter namespaces + all filters 2010-11-03 09:47:21 -07:00
Igor Minar aec3c8478c binding() should return value for input/text area, innerHTML for the rest 2010-11-03 09:47:21 -07:00
Igor Minar 5c887ddb66 adding textarea() DSL for scenario runner 2010-11-03 09:47:21 -07:00
Igor Minar 0bd4a473a7 adding regular expressions support for binding() DSL 2010-11-03 09:44:54 -07:00
Elliott Sprehn dcf76e6816 Provide better sandbox error messages, and disallow running from file:// URLs 2010-11-02 11:27:54 -07:00
Elliott Sprehn 56a3d52f45 Make future names consistent and handle falsy values in jQuery generated methods properly 2010-11-02 11:20:41 -07:00
Elliott Sprehn faa7d81b67 Add browser().reload() to simulate a refresh from a user 2010-11-01 17:24:24 -07:00
Elliott Sprehn 6bb2cd6ee2 Provide browser DSL with location() to expect the iframe URL parts. Also move navigateTo() under the browser DSL. 2010-11-01 15:21:37 -07:00
Vojta Jina 2d61040fb0 Small refactor in $location service
Added new inner method updateLastLocation()
2010-10-31 21:37:54 -07:00
Vojta Jina 99f25050a3 Fixing issue #98 (infinite loop when location hash set empty)
Added tests and fixed the issue.

Closes #98
2010-10-31 21:37:33 -07:00
Misko Hevery ba5f8ee27f fix typo, and change a list to string for more efficient compression. 2010-10-31 15:04:30 -07:00
Elliott Sprehn 9a532002cf Auto generate all the jQuery get/set methods 2010-10-29 12:19:22 -07:00
Elliott Sprehn 5524d2b0fb Check if file exists (not a 404) and that document is accessible and not using file:// URLs in Application 2010-10-29 11:40:56 -07:00
Igor Minar d4839bac32 adding watchr config file watchr.rb 2010-10-29 10:47:51 -07:00
Igor Minar 602369c6a2 creating logs/ and tmp/ dirs 2010-10-29 10:47:06 -07:00
Andres Ornelas 34909520ae add optional label to dsl with selectors to improve test and output readability
e.g.
Before:
   code:   element('.actions ul li a').click();
   output: element .actions ul li a click
After
   code:   element('.actions ul li a', "'Configuration' link").click();
   output: element 'Configuration' link ( .actions ul li a ) click
2010-10-28 15:21:02 -07:00
Elliott Sprehn 92e31b556f Correctly fail tests if no binding matches and add better test cases for failure behavior. 2010-10-27 17:56:44 -07:00
Misko Hevery 62c0e5c460 Fix failing tests for ie, and mark elements as ng-widget, ng-directive, and ng-binding 2010-10-27 15:42:46 -07:00
Igor Minar c67af8a038 rename src/Parser.js to src/parser.js 2010-10-27 15:32:30 -07:00
Elliott Sprehn 2da1de5a6d Revert performance optimization of using setTimeout in SpecRunner every 10 steps. Breaks loading partials 2010-10-27 13:41:31 -07:00
Igor Minar ee8465bf10 updating version.yaml for the 0.9.2 iteration 2010-10-27 07:38:18 -07:00
Misko Hevery 9c6794a044 start new release 0.9.2 faunal-mimicry 2010-10-26 22:48:34 -07:00
Igor Minar 0ffb47bdb6 cutting the 0.9.1 repulsion-field release 2010-10-26 22:18:25 -07:00
Igor Minar ec885489a2 updating CHANGELOG.md for the 0.9.1 release 2010-10-26 22:17:37 -07:00
Igor Minar 01c52e92b1 Adding e2e tests for the PersonalLog app
- added scenario runner
- added scenario specs
- cookie cleaning DSL
- making rmLog independent on ordering in the view
2010-10-26 21:58:00 -07:00
Misko Hevery 943377a091 fix closure compile warnings 2010-10-26 21:06:24 -07:00
Igor Minar 72b7a1c531 adding missing angular-mocksSpec.js file 2010-10-26 16:44:59 -07:00
Misko Hevery 9c0225512c fixes IE related failures, and form submit event handling in ie 2010-10-26 16:33:59 -07:00
Elliott Sprehn 40d7e66f40 Lots of bug fixes in the scenario runner and a bunch of new features.
- By default the runner now creates multiple output formats as it runs. Nodes are created in the DOM with ids: json, xml, and html.

ex. $('#json').html() => json output of the runner
ex. $('#xml').html() => json output of the runner

$result is also an object tree result.

The permitted formats are html,json,xml,object.

If you don't want certain formats you can select specific ones with the new ng:scenario-output attribute on the script tag.

<script src="angular-scenario.js" ng:scenario-output="xml,json">

- Added element(...).count() that returns the number of matching elements for the selector.

- repeater(...).count() now returns 0 if no elements matched which can be used to check if a repeater is empty.

- Added toBe() matcher that does strict equality with ===

- Implement iit and ddescribe. If iit() is used instead of it() then only that test will run. If ddescribe() is used instead of describe() them only it() statements inside of it will run. Several iit/ddescribe() blocks can be used to run isolated tests.

- Implement new event based model for SpecRunner. You can now listen for events in the runner. This is useful for writing your own UI or connecting a remote process (ex. WebDriver). Event callbacks execute on the Runner instance.

Events, if fired, will always be in the below order. All events always happen
except for Failure and Error events which only happen in error conditions.

Events:
  RunnerBegin
  SpecBegin(spec)
  StepBegin(spec, step)
  StepError(spec, step, error)
  StepFailure(spec, step, error)
  StepEnd(spec, step)
  SpecError(spec, step, error)
  SpecEnd(spec)
  RunnerEnd

- Only allow the browser to repaint every 10 steps. Cuts 700ms off Firefox in benchmark, 200ms off Chrome.

- Bug Fix: Manually navigate anchors on click since trigger wont work in Firefox.
2010-10-26 15:17:57 -07:00
Misko Hevery 1d52349440 Reverted change 841013a4c4 which does not work on all browsers 2010-10-26 14:25:01 -07:00
Igor Minar 3eb0c8bc67 Simplify dateGetter() within the date filter. 2010-10-26 13:47:50 -07:00
Igor Minar 42855e4363 Simplify implementation of angular.String.toDate() 2010-10-26 13:47:50 -07:00
Igor Minar 4c61fc01f9 Add TzDate to angular-mocks.js
TzDate is a Date-like type that is independent from the timezone
settings of the machine on which TzDate instances are created.
This property makes it ideal for testing code that deals with
timezones in a manner that makes the code portable between timezones.

Closes #81
2010-10-26 13:47:50 -07:00
Misko Hevery 4fdab37659 create HTML sanitizer to allow inclusion of untrusted HTML in safe manner.
Sanitization works in two phases:
 1) We parse the HTML into sax-like events (start, end, chars).
    HTML parsing is very complex, and so it may very well be that what
    most browser consider valid HTML may not pares properly here,
    but we do best effort. We treat this parser as untrusted.
 2) We have safe sanitizeWriter which treats its input (start, end, chars)
    as untrusted content and escapes everything. It only allows elements
    in the whitelist and only allows attributes which are whitelisted.
    Any attribute value must not start with 'javascript:'. This check
    is performed after escaping for entity (&xAB; etc..) and ignoring
    any whitespace.

 - Correct linky filter to use safeHtmlWriter
 - Correct html filter to use safeHtmlWriter

Close #33; Close #34
2010-10-26 13:41:07 -07:00
Igor Minar 841013a4c4 Add millisecond support for date filter
Date filter should translate input which is a number (or number
string) into a date.
2010-10-25 10:44:03 -07:00
Igor Minar 4e9a2aa10e Revert "added support for treating numbers as date in miliseconds"
This reverts commit 1391f19fb4.
2010-10-23 18:26:47 -07:00
Igor Minar 4fc2141458 improving the PersonalLog app
- adding jsdocs and comments
- logs should be ordered in inverse order
2010-10-23 16:25:53 -07:00
Igor Minar 5b40e87ac6 personalLog demo - initial version with spec 2010-10-23 14:38:08 -07:00
Misko Hevery 1391f19fb4 added support for treating numbers as date in miliseconds 2010-10-23 14:38:08 -07:00
Igor Minar 04a4d8b061 adding ng:submit directive for use with forms
- allows for binding angular expressions to onsubmit events
- prevent default submit action (page reload)
2010-10-23 14:22:54 -07:00
Igor Minar bbd87c9425 simplifying ng:click spec 2010-10-23 14:22:54 -07:00
Elliott Sprehn 64063b5d41 Fix issue where directories don't have a slash on the end and allow specifying a different port 2010-10-23 14:22:54 -07:00
Igor Minar 833e0ae343 $cookieStore should not be a global service
you must use $inject to $inject it as any other non-global service
2010-10-23 14:22:30 -07:00
Misko Hevery d74ef497de Fix for getting into recursive $eval on scope. Close #59
It sort of worked since the browser would throw stack too deep
exception and the angular would then print the error to console.
So as long as you did not have console open you would not notice
this as an error.
2010-10-23 13:42:11 -07:00
Misko Hevery 6ddcf91861 Fix test which was causing the Chrome runner to fail. Upgraded JSTD to latest. Cleanup whitespace. 2010-10-23 13:12:45 -07:00
Misko Hevery 8a867cee22 Workaround for http://bugs.jquery.com/ticket/7292 2010-10-22 22:46:51 -07:00
Misko Hevery 68217d427c Horrible IE + jQuery hack to make the tests pass 2010-10-22 16:40:04 -07:00
Misko Hevery 1efef67b5f corrected buzz.html to use ng:src on images to prevent sporious browser requests/errors 2010-10-22 14:27:03 -07:00
Misko Hevery a6cfa43c19 fixed issue with ie .data() method failing tests 2010-10-22 14:26:18 -07:00
Elliott Sprehn b41bc98c54 Better nodeserver that implements an HTTP server more completely 2010-10-22 00:26:05 +08:00
Misko Hevery aaabeb8c5e fixed more ie test failures 2010-10-20 23:51:49 -07:00
Misko Hevery 05d4971abb fix some of the failing ie tests 2010-10-20 23:17:59 -07:00
Igor Minar c6107fe8ac Rakefile should create the build directory when it is needed and doesn't exist 2010-10-20 17:14:31 -07:00
Igor Minar 68f074c299 adding CHANGELOG.md with 0.9.0 release notes 2010-10-20 17:00:36 -07:00
Igor Minar c53a37ed91 preparing 0.9.1 repulsion-field iteration 2010-10-20 15:52:55 -07:00
147 changed files with 12599 additions and 1764 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ui.externaltools.ProgramBuilderLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_BUILDER_ENABLED" value="true"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_BUILD_SCOPE" value="${working_set:&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;launchConfigurationWorkingSet editPageId=&quot;org.eclipse.ui.resourceWorkingSetPage&quot; factoryID=&quot;org.eclipse.ui.internal.WorkingSetFactory&quot; id=&quot;1262905463390_2&quot; label=&quot;workingSet&quot; name=&quot;workingSet&quot;&gt;&#10;&lt;item factoryID=&quot;org.eclipse.ui.internal.model.ResourceFactory&quot; path=&quot;/angular.js/test&quot; type=&quot;2&quot;/&gt;&#10;&lt;item factoryID=&quot;org.eclipse.ui.internal.model.ResourceFactory&quot; path=&quot;/angular.js/src&quot; type=&quot;2&quot;/&gt;&#10;&lt;/launchConfigurationWorkingSet&gt;}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/angular.js}/test.sh"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,incremental,auto,"/>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ui.externaltools.ProgramBuilderLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${working_set:&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;resources&gt;&#10;&lt;item path=&quot;/angular.js/build&quot; type=&quot;2&quot;/&gt;&#10;&lt;/resources&gt;}"/>
<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_BUILDER_ENABLED" value="false"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_BUILD_SCOPE" value="${working_set:&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;resources&gt;&#10;&lt;item path=&quot;/angular.js/docs&quot; type=&quot;2&quot;/&gt;&#10;&lt;item path=&quot;/angular.js/src&quot; type=&quot;2&quot;/&gt;&#10;&lt;/resources&gt;}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/angular.js/gen_docs.sh}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,incremental,auto,"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:/angular.js}"/>
</launchConfiguration>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ui.externaltools.ProgramLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${working_set:&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;resources&gt;&#10;&lt;item path=&quot;/angular.js/docs&quot; type=&quot;2&quot;/&gt;&#10;&lt;/resources&gt;}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LAUNCH_CONFIGURATION_BUILD_SCOPE" value="${none}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/angular.js/gen_docs.sh}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:/angular.js}"/>
</launchConfiguration>
+3
View File
@@ -1,3 +1,6 @@
build/
angularjs.netrc
jstd.log
.DS_Store
regression/temp.html
.idea/workspace.xml
-1
View File
@@ -1 +0,0 @@
workspace.xml
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Settings><!--This file was automatically generated by Ruby plugin.
You are allowed to:
1. Remove rake task
2. Add existing rake tasks
To add existing rake tasks automatically delete this file and reload the project.
--><RakeGroup description="" fullCmd="" taksId="rake"><RakeTask description="Compile JavaScript" fullCmd="compile" taksId="compile" /><RakeTask description="Generate Externs" fullCmd="compileexterns" taksId="compileexterns" /><RakeTask description="Lint" fullCmd="lint" taksId="lint" /><RakeGroup description="" fullCmd="" taksId="server"><RakeTask description="Run JsTestDriver Server" fullCmd="server:start" taksId="start" /><RakeTask description="Run JavaScript tests against the server" fullCmd="server:test" taksId="test" /></RakeGroup><RakeTask description="Run JavaScript tests" fullCmd="test" taksId="test" /><RakeTask description="" fullCmd="default" taksId="default" /></RakeGroup></Settings>
Generated
+67
View File
@@ -0,0 +1,67 @@
IMPORTANT: THIS IS TO CERTIFY THE RIGHT TO USE THE JETBRAINS SOFTWARE PRODUCT, GRANTED BY JETBRAINS S.R.O. UNDER THE TERMS AND CONDITIONS OF THE LICENSE AGREEMENT INCLUDED WITH THE SOFTWARE. PLEASE SAVE A COPY OF THIS EMAIL FOR FUTURE REFERENCES.
========LICENSE DETAILS========
Type: Open Source License
Reference No*: LC-93161-D352729080
Date of Issue: 3 November 2010
Expiration Date: 3 November 2011
Number of Authorized Users: not limited
* Please quote this reference when contacting JetBrains
===========LICENSEE============
Name: angularjs
Customer ID: 93161
Address:
=======SOFTWARE PRODUCT========
Product Name: WebStorm
Licensed Version: 1.0 and any new product release which is made generally available before 3 November 2011
The software is shipped electronically and is available for download from:
http://www.jetbrains.com/webstorm/download/
Your WebStorm license includes a 1-year upgrade subscription qualifying you for free upgrades to any new versions of WebStorm released by JetBrains during your upgrade subscription period starting on your license issue date and ending on 3 November 2011. To check availability of the new versions of WebStorm, please visit http://www.jetbrains.com.
For running any new version of WebStorm released by JetBrains during your upgrade subscription period, please use the included below licensing details.
You can renew your upgrade subscription before or after its expiration. Your new subscription period will start on the date following the expiration date of your current upgrade subscription.
=========INSTALLATION==========
Run WebStorm and follow the Installation Wizard's instructions. To register for use of the software or change your existing registration details, go to Help/Register menu of the software and enter the included below the User Name and License Key(s) into the registration dialog:
User Name: angularjs
===== LICENSE BEGIN =====
93161-03112010
00000jBsEx59XVlc79fV"aAqWXQ09e
jQsg5TNp5X4HGhc10LNBdu!!ejRcFG
7h3S6T09YcRWs23TH0RgaM87!HqmQo
===== LICENSE END =====
===DOCUMENTATION AND SUPPORT===
WebStorm documentation:
http://www.jetbrains.com/webstorm/documentation/
Available support resources:
http://www.jetbrains.com/support/
Technical support contact:
support@jetbrains.com
Contact for the license renewal requests:
opensource@jetbrains.com
For questions, please contact:
sales@jetbrains.com
JetBrains Sales Team
http://www.jetbrains.com
"Develop with pleasure!"
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUBY_MODULE" version="4">
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
+1 -4
View File
@@ -3,10 +3,7 @@
<component name="DependencyValidationManager">
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</component>
<component name="ProjectDetails">
<option name="projectName" value="master" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Ruby SDK 1.8.7 (/usr/bin/ruby)" project-jdk-type="RUBY_SDK" />
<component name="ProjectRootManager" version="2" />
<component name="SvnBranchConfigurationManager">
<option name="mySupportsUserInfoFilter" value="true" />
</component>
+1 -1
View File
@@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/master.iml" filepath="$PROJECT_DIR$/.idea/master.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/angular.js.iml" filepath="$PROJECT_DIR$/.idea/angular.js.iml" />
</modules>
</component>
</project>
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="USE_SAME_INDENTS" value="true" />
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</value>
</option>
<ADDITIONAL_INDENT_OPTIONS fileType="js">
<option name="INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</ADDITIONAL_INDENT_OPTIONS>
<ADDITIONAL_INDENT_OPTIONS fileType="jsp">
<option name="INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</ADDITIONAL_INDENT_OPTIONS>
<ADDITIONAL_INDENT_OPTIONS fileType="sass">
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</ADDITIONAL_INDENT_OPTIONS>
<ADDITIONAL_INDENT_OPTIONS fileType="xml">
<option name="INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</ADDITIONAL_INDENT_OPTIONS>
<ADDITIONAL_INDENT_OPTIONS fileType="yml">
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
</ADDITIONAL_INDENT_OPTIONS>
</value>
</option>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="gen_docs" type="BashConfigurationType" factoryName="Bash">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="PARENT_ENVS" value="true" />
<envs />
<module name="angular.js" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/gen_docs.sh" />
<option name="PARAMETERS" value="" />
<RunnerSettings RunnerId="BashRunner" />
<ConfigurationWrapper RunnerId="BashRunner" />
<method />
</configuration>
</component>
+15
View File
@@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="rake compile" type="BashConfigurationType" factoryName="Bash">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="INTERPRETER_PATH" value="/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="PARENT_ENVS" value="true" />
<envs />
<module name="angular.js" />
<option name="SCRIPT_NAME" value="/usr/bin/rake" />
<option name="PARAMETERS" value="compile" />
<RunnerSettings RunnerId="BashRunner" />
<ConfigurationWrapper RunnerId="BashRunner" />
<method />
</configuration>
</component>
Generated
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+10
View File
@@ -20,6 +20,16 @@
</dictionary>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.ui.externaltools.ExternalToolBuilder</name>
<triggers>auto,full,incremental,</triggers>
<arguments>
<dictionary>
<key>LaunchConfigHandle</key>
<value>&lt;project&gt;/.externalToolBuilders/docs.launch</value>
</dictionary>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
+2 -1
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="test/" kind="src" path="src"/>
<classpathentry kind="src" path="src/test"/>
<classpathentry excluding="docs-data.js|docs-scenario.js" kind="src" path="docs"/>
<classpathentry excluding="test/" kind="src" path="test"/>
<classpathentry kind="src" path="test/test"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
<classpathentry kind="output" path=""/>
</classpath>
+135
View File
@@ -0,0 +1,135 @@
# <angular/> 0.9.4 total-recall (2010-11-18) #
### Docs
- searchable docs
- UI improvements
- we now have ~85% of the wiki docs migrated to ng docs
- some but not all docs were updated along the way
### Api
- ng:include now supports `onload` attribute (commit cc749760)
### Misc
- Better error handling - compilation exception now contain stack trace (commit b2d63ac4)
# <angular/> 0.9.3 cold-resistance (2010-11-10) #
### Docs
- prettier docs app with syntax highlighting for examples, etc
- added documentation, examples and scenario tests for many more apis including:
- all directives
- all formatters
- all validators
- some widgets
### Api
- date filter now accepts strings that angular.String.toDate can convert to Date objects
- angular.String.toDate supports ISO8061 formated strings with all time fractions being optional
- ng:repeat now exposes $position with values set to 'first', 'middle' or 'last'
- ng:switch now supports ng:switch-default as fallback switch option
### Breaking changes
- we now support ISO 8601 extended format datetime strings (YYYY-MM-DDTHH:mm:ss.SSSZ) as defined
in EcmaScript 5 throughout angular. This means that the following apis switched from
YYYY-MM-DDTHH:mm:ssZ to YYYY-MM-DDTHH:mm:ss.SSSZ (note the added millis) when representing dates:
- angular.Date.toString
- angular.String.fromDate
- JSON serialization and deserialization (used by json filter, $xhr and $resource)
- removed SSN validator. It's unlikely that most people will need it and if they do, it can be added
simple RegExp validator.
# <angular/> 0.9.2 faunal-mimicry (2010-11-03) #
### Docs
- created documentation framework based on jsdoc syntax (commit 659af29a)
- jsdoc parser
- template generator
- json generator
- angular doc viewer app
- scenario runner for all example code
- documentation for all angular filters (commits 1fe7e3a1 & 1ba8c2a33)
- docs
- example code
- scenario tests for example code
### Testability
#### Scenario Runner
- binding DSL in Scenario can now match bindings without specifying filters
- dsl statements now accept a label argument to make test output more readable (issue #94)
- dsl element() statement now implements most of the jQuery API (issue #106)
- new browser() dsl statement for getting info about the emulated browser running the app
(issue #109)
- scenario runner is now compatible with IE8 (issue #93)
- scenarior runner checks if URL would return a non-success status code (issue #100)
- binding() DSL now accepts regular expressions
- new textarea() scenario runner DSL for entering text into textareas
### Misc
- lots of small bugfixes
### Breaking changes
#### Scenario Runner
- navigating to about:blank is no longer supported. It results in a sandbox error
- navigateTo() is now browser().navigateTo(). Old code must be updated
- file:// URLs are no longer supported for running a scenario. You must use a web server that
implements HEAD
# <angular/> 0.9.1 repulsion-field (2010-10-26) #
### Security
- added html sanitizer to fix the last few known security issues (issues #33 and #34)
### API
- new ng:submit directive for creating onSubmit handlers on forms (issue #76)
- the date filter now accepts milliseconds as well as date strings (issue #78)
- the html filter now supports 'unsafe' option to bypass html sanitization
### Testability
- lots of improvements related to the scenario runner (commit 40d7e66f)
### Demo
- added a new demo application: Personal Log (src example/personalLog)
### Chores
- lots of fixes to get all tests pass on IE
- added TzDate type to allow us to create timezone idependent tests (issue #88)
### Breaking changes
- $cookieStore service is not globally published any more, if you use it, you must request it via
$inject as any other non-global service
- html filter now sanitizes html content for XSS attacks which may result in different behavior
# <angular/> 0.9.0 dragon-breath (2010-10-20) #
### Security
- angular.fromJson not safer (issue #57)
- readString consumes invalid escapes (issue #56)
- use new Function instead of eval (issue #52)
### Speed
- css cleanup + inline all css and images in the main js (issue #64)
### Testability
- initial version of the built-in end-to-end scenario runner (issues #50, #67, #70)
### API
- allow ng:controller nesting (issue #39)
- new built-in date format filter (issue #45)
- $location needs method you call on updates (issue #32)
### Chores
- release versioning + file renaming (issue #69)
### Breaking changes
- $location.parse was replaced with $location.update
- all css and img files were inlined into the main js file, to support IE7 and older app must host
angular-ie-compat.js file
### Big Thanks to Our Community Contributors
- Vojta Jina
+38 -10
View File
@@ -6,9 +6,10 @@ ANGULAR = [
'src/Compiler.js',
'src/Scope.js',
'src/Injector.js',
'src/Parser.js',
'src/parser.js',
'src/Resource.js',
'src/Browser.js',
'src/sanitizer.js',
'src/jqLite.js',
'src/apis.js',
'src/filters.js',
@@ -26,12 +27,16 @@ ANGULAR_SCENARIO = [
'src/scenario/Application.js',
'src/scenario/Describe.js',
'src/scenario/Future.js',
'src/scenario/HtmlUI.js',
'src/scenario/ObjectModel.js',
'src/scenario/Describe.js',
'src/scenario/Runner.js',
'src/scenario/SpecRunner.js',
'src/scenario/dsl.js',
'src/scenario/matchers.js',
'src/scenario/output/Html.js',
'src/scenario/output/Json.js',
'src/scenario/output/Xml.js',
'src/scenario/output/Object.js',
]
BUILD_DIR = 'build'
@@ -39,6 +44,12 @@ BUILD_DIR = 'build'
task :default => [:compile, :test]
desc 'Init the build workspace'
task :init do
FileUtils.mkdir(BUILD_DIR) unless File.directory?(BUILD_DIR)
end
desc 'Clean Generated Files'
task :clean do
FileUtils.rm_r(BUILD_DIR, :force => true)
@@ -47,7 +58,7 @@ end
desc 'Compile Scenario'
task :compile_scenario do
task :compile_scenario => :init do
deps = [
'lib/jquery/jquery-1.4.2.js',
@@ -67,11 +78,8 @@ task :compile_scenario do
end
desc 'Generate IE css js patch'
task :generate_ie_compat do
task :generate_ie_compat => :init do
css = File.open('css/angular.css', 'r') {|f| f.read }
# finds all css rules that contain backround images and extracts the rule name(s), content type of
@@ -127,7 +135,7 @@ end
desc 'Compile JavaScript'
task :compile => [:compile_scenario, :generate_ie_compat] do
task :compile => [:init, :compile_scenario, :generate_ie_compat] do
deps = [
'src/angular.prefix',
@@ -148,8 +156,14 @@ task :compile => [:compile_scenario, :generate_ie_compat] do
end
desc 'Generate docs'
task :docs do
`node docs/collect.js`
end
desc 'Create angular distribution'
task :package => :compile do
task :package => [:clean, :compile, :docs] do
v = YAML::load( File.open( 'version.yaml' ) )['version']
match = v.match(/^([^-]*)(-snapshot)?$/)
version = match[1] + (match[2] ? ('-' + %x(git rev-parse HEAD)[0..7]) : '')
@@ -170,6 +184,20 @@ task :package => :compile do
FileUtils.cp(src, pkg_dir + '/' + dest)
end
FileUtils.cp_r path_to('docs'), "#{pkg_dir}/docs-#{version}"
File.open("#{pkg_dir}/docs-#{version}/index.html", File::RDWR) do |f|
text = f.read
f.rewind
f.write text.sub('angular.min.js', "angular-#{version}.min.js")
end
File.open("#{pkg_dir}/docs-#{version}/docs-scenario.html", File::RDWR) do |f|
text = f.read
f.rewind
f.write text.sub('angular-scenario.js', "angular-scenario-#{version}.js")
end
%x(tar -czf #{path_to(tarball)} -C #{path_to('pkg')} .)
puts "Package created: #{path_to(tarball)}"
@@ -248,5 +276,5 @@ end
# returns path to the file in the build directory
#
def path_to(filename)
return File.join(BUILD_DIR, filename)
return File.join(BUILD_DIR, *filename)
end
+21 -8
View File
@@ -8,6 +8,15 @@ body {
font-size: 14px;
}
#system-error {
font-size: 1.5em;
text-align: center;
}
#json, #xml {
display: none;
}
#header {
position: fixed;
width: 100%;
@@ -32,7 +41,7 @@ body {
height: 30px;
}
#frame h2,
#application h2,
#specs h2 {
margin: 0;
padding: 0.5em;
@@ -45,26 +54,26 @@ body {
}
#header,
#frame,
#application,
.test-info,
.test-actions li {
overflow: hidden;
}
#frame {
#application {
margin: 10px;
}
#frame iframe {
#application iframe {
width: 100%;
height: 758px;
}
#frame .popout {
#application .popout {
float: right;
}
#frame iframe {
#application iframe {
border: none;
}
@@ -154,6 +163,10 @@ body {
margin-left: 6em;
}
.test-describe {
padding-bottom: 0.5em;
}
.test-describe .test-describe {
margin: 5px 5px 10px 2em;
}
@@ -178,11 +191,11 @@ body {
}
#specs h2,
#frame h2 {
#application h2 {
background-color: #efefef;
}
#frame {
#application {
border: 1px solid #BABAD1;
}
+1
View File
@@ -8,6 +8,7 @@
border: 2px solid #FF0000;
font-family: "Courier New", Courier, monospace;
font-size: smaller;
white-space: pre;
}
.ng-validation-error {
+66
View File
@@ -0,0 +1,66 @@
function noop(){}
function chain(delegateFn, explicitDone){
var onDoneFn = noop;
var onErrorFn = noop;
var waitForCount = 1;
delegateFn = delegateFn || noop;
var stackError = new Error('capture stack');
function decrementWaitFor() {
waitForCount--;
if (waitForCount == 0)
onDoneFn();
}
function self(){
try {
return delegateFn.apply(self, arguments);
} catch (error) {
self.error(error);
} finally {
if (!explicitDone)
decrementWaitFor();
}
};
self.onDone = function(callback){
onDoneFn = callback;
return self;
};
self.onError = function(callback){
onErrorFn = callback;
return self;
};
self.waitFor = function(callback){
if (waitForCount == 0)
throw new Error("Can not wait on already called callback.");
waitForCount++;
return chain(callback).onDone(decrementWaitFor).onError(self.error);
};
self.waitMany = function(callback){
if (waitForCount == 0)
throw new Error("Can not wait on already called callback.");
waitForCount++;
return chain(callback, true).onDone(decrementWaitFor).onError(self.error);
};
self.done = function(callback){
decrementWaitFor();
};
self.error = function(error) {
var stack = stackError.stack.split(/\n\r?/).splice(2);
var nakedStack = [];
stack.forEach(function(frame){
if (!frame.match(/callback\.js:\d+:\d+\)$/))
nakedStack.push(frame);
});
error.stack = error.stack + '\nCalled from:\n' + nakedStack.join('\n');
onErrorFn(error);
};
return self;
}
exports.chain = chain;
+393
View File
@@ -0,0 +1,393 @@
require.paths.push("./lib");
require.paths.push(__dirname);
var fs = require('fs'),
spawn = require('child_process').spawn,
mustache = require('mustache'),
callback = require('callback'),
Showdown = require('showdown').Showdown;
var documentation = {
pages:[],
byName: {}
};
var keywordPages = [];
var SRC_DIR = "docs/";
var OUTPUT_DIR = "build/docs/";
var NEW_LINE = /\n\r?/;
var work = callback.chain(function () {
console.log('Parsing Angular Reference Documentation');
mkdirPath(OUTPUT_DIR, work.waitFor(function(){
findJsFiles('src', work.waitMany(function(file) {
//console.log('reading', file, '...');
findNgDoc(file, work.waitMany(function(doc) {
parseNgDoc(doc);
processNgDoc(documentation, doc);
}));
}));
}));
}).onError(function(err){
console.log('ERROR:', err.stack || err);
}).onDone(function(){
keywordPages.sort(function(a,b){
// supper ugly comparator that orders all utility methods and objects before all the other stuff
// like widgets, directives, services, etc.
// Mother of all beatiful code please forgive me for the sin that this code certainly is.
if (a.name === b.name) return 0;
if (a.name === 'angular') return -1;
if (b.name === 'angular') return 1;
function namespacedName(page) {
return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name;
}
var namespacedA = namespacedName(a),
namespacedB = namespacedName(b);
return namespacedA < namespacedB ? -1 : 1;
});
writeDoc(documentation.pages);
mergeTemplate('docs-data.js', 'docs-data.js', {JSON:JSON.stringify(keywordPages)}, callback.chain());
mergeTemplate('docs-scenario.js', 'docs-scenario.js', documentation, callback.chain());
copy('docs-scenario.html', callback.chain());
copy('index.html', callback.chain());
copy('docs.css', callback.chain());
mergeTemplate('docs.js', 'docs.js', documentation, callback.chain());
mergeTemplate('doc_widgets.css', 'doc_widgets.css', documentation, callback.chain());
mergeTemplate('doc_widgets.js', 'doc_widgets.js', documentation, callback.chain());
console.log('DONE');
});
if (!this.testmode) work();
////////////////////
function keywords(text){
var keywords = {};
var words = [];
var tokens = text.toLowerCase().split(/[,\.\`\'\"\s]+/mg);
tokens.forEach(function(key){
var match = key.match(/^(([a-z]|ng\:)[\w\_\-]{2,})/);
if (match){
key = match[1];
if (!keywords[key]) {
keywords[key] = true;
words.push(key);
}
}
});
words.sort();
return words.join(' ');
}
function noop(){}
function mkdirPath(path, callback) {
var parts = path.split(/\//);
path = '.';
(function next(){
if (parts.length) {
path += '/' + parts.shift();
fs.mkdir(path, 0777, next);
} else {
callback();
}
})();
}
function copy(name, callback){
fs.readFile(SRC_DIR + name, callback.waitFor(function(err, content){
if (err) return this.error(err);
fs.writeFile(OUTPUT_DIR + name, content, callback);
}));
}
function mergeTemplate(template, output, doc, callback){
fs.readFile(SRC_DIR + template,
callback.waitFor(function(err, template){
if (err) return this.error(err);
var content = mustache.to_html(template.toString(), doc);
fs.writeFile(OUTPUT_DIR + output, content, callback);
}));
}
function trim(text) {
var MAX = 9999;
var empty = RegExp.prototype.test.bind(/^\s*$/);
var lines = text.split('\n');
var minIndent = MAX;
lines.forEach(function(line){
minIndent = Math.min(minIndent, indent(line));
});
for ( var i = 0; i < lines.length; i++) {
lines[i] = lines[i].substring(minIndent);
}
// remove leading lines
while (empty(lines[0])) {
lines.shift();
}
// remove trailing
while (empty(lines[lines.length - 1])) {
lines.pop();
}
return lines.join('\n');
function indent(line) {
for(var i = 0; i < line.length; i++) {
if (line.charAt(i) != ' ') {
return i;
}
}
return MAX;
}
}
function unknownTag(doc, name) {
var error = "[" + doc.raw.file + ":" + doc.raw.line + "]: unknown tag: " + name;
console.log(error);
throw new Error(error);
}
function valueTag(doc, name, value) {
doc[name] = value;
}
function escapedHtmlTag(doc, name, value) {
doc[name] = value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function markdownTag(doc, name, value) {
doc[name] = markdown(value.replace(/^#/gm, '##')).
replace(/\<pre\>/gmi, '<div ng:non-bindable><pre class="brush: js; html-script: true;">').
replace(/\<\/pre\>/gmi, '</pre></div>');
}
R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m
// 1 123 3 4 42
function markdown(text) {
var parts = text.split(/(<pre>[\s\S]*?<\/pre>)/),
match;
parts.forEach(function(text, i){
if (!text.match(/^<pre>/)) {
text = text.replace(/<angular\/>/gm, '<tt>&lt;angular/&gt;</tt>');
text = new Showdown.converter().makeHtml(text);
while (match = text.match(R_LINK)) {
text = text.replace(match[0], '<a href="#!' + match[1] + '"><code>' +
(match[4] || match[1]) +
'</code></a>');
}
parts[i] = text;
}
});
return parts.join('');
}
function markdownNoP(text) {
var lines = markdown(text).split(NEW_LINE);
var last = lines.length - 1;
lines[0] = lines[0].replace(/^<p>/, '');
lines[last] = lines[last].replace(/<\/p>$/, '');
return lines.join('\n');
}
function requiresTag(doc, name, value) {
doc.requires = doc.requires || [];
doc.requires.push({name: value});
}
function propertyTag(doc, name, value) {
doc[name] = doc[name] || [];
var match = value.match(/^({(\S+)}\s*)?(\S+)(\s+(.*))?/);
if (match) {
var tag = {
type: match[2],
name: match[3],
description: match[5] || false
};
} else {
throw "[" + doc.raw.file + ":" + doc.raw.line +
"]: @" + name + " must be in format '{type} name description' got: " + value;
}
return doc[name].push(tag);
}
function returnsTag(doc, name, value) {
var match = value.match(/^{(\S+)}\s+(.*)?/);
if (match) {
var tag = {
type: match[1],
description: markdownNoP(match[2]) || false
};
} else {
throw "[" + doc.raw.file + ":" + doc.raw.line +
"]: @" + name + " must be in format '{type} description' got: " + value;
}
return doc[name] = tag;
}
var TAG = {
ngdoc: valueTag,
example: escapedHtmlTag,
scenario: valueTag,
namespace: valueTag,
css: valueTag,
see: valueTag,
deprecated: valueTag,
workInProgress: function(doc, name, value) {
doc[name] = {description: markdown(value)};
},
usageContent: valueTag,
'function': valueTag,
description: markdownTag,
TODO: markdownTag,
paramDescription: markdownTag,
exampleDescription: markdownTag,
element: valueTag,
methodOf: valueTag,
name: function(doc, name, value) {
doc.name = value;
doc.shortName = value.split(/\./).pop();
doc.depth = value.split(/\./).length - 1;
},
param: function(doc, name, value){
doc.param = doc.param || [];
doc.paramRest = doc.paramRest || [];
var match = value.match(/^{([^}=]+)(=)?}\s+(([^\s=]+)|\[(\S+)=([^\]]+)\])\s+(.*)/);
// 1 12 2 34 4 5 5 6 6 3 7 7
if (match) {
var param = {
type: match[1],
name: match[5] || match[4],
optional: !!match[2],
'default':match[6],
description:markdownNoP(value.replace(match[0], match[7]))
};
doc.param.push(param);
if (!doc.paramFirst) {
doc.paramFirst = param;
} else {
doc.paramRest.push(param);
}
} else {
throw "[" + doc.raw.file + ":" + doc.raw.line +
"]: @param must be in format '{type} name=value description' got: " + value;
}
},
property: propertyTag,
requires: requiresTag,
returns: returnsTag
};
function parseNgDoc(doc){
var atName;
var atText;
var match;
doc.raw.text.split(NEW_LINE).forEach(function(line, lineNumber){
if (match = line.match(/^\s*@(\w+)(\s+(.*))?/)) {
// we found @name ...
// if we have existing name
if (atName) {
(TAG[atName] || unknownTag)(doc, atName, trim(atText.join('\n')));
}
atName = match[1];
atText = [];
if(match[3]) atText.push(match[3]);
} else {
if (atName) {
atText.push(line);
} else {
// ignore
}
}
});
if (atName) {
(TAG[atName] || unknownTag)(doc, atName, atText.join('\n'));
}
}
function findNgDoc(file, callback) {
fs.readFile(file, callback.waitFor(function(err, content){
var lines = content.toString().split(NEW_LINE);
var doc;
var match;
var inDoc = false;
lines.forEach(function(line, lineNumber){
lineNumber++;
// is the comment starting?
if (!inDoc && (match = line.match(/^\s*\/\*\*\s*(.*)$/))) {
line = match[1];
inDoc = true;
doc = {raw:{file:file, line:lineNumber, text:[]}};
}
// are we done?
if (inDoc && line.match(/\*\//)) {
doc.raw.text = doc.raw.text.join('\n');
doc.raw.text = doc.raw.text.replace(/^\n/, '');
if (doc.raw.text.match(/@ngdoc/)){
callback(doc);
}
doc = null;
inDoc = false;
}
// is the comment add text
if (inDoc){
doc.raw.text.push(line.replace(/^\s*\*\s?/, ''));
}
});
callback.done();
}));
}
function findJsFiles(dir, callback){
fs.readdir(dir, callback.waitFor(function(err, files){
if (err) return this.error(err);
files.forEach(function(file){
var path = dir + '/' + file;
fs.lstat(path, callback.waitFor(function(err, stat){
if (err) return this.error(err);
if (stat.isDirectory())
findJsFiles(path, callback.waitMany(callback));
else if (/\.js$/.test(path))
callback(path);
}));
});
callback.done();
}));
}
function processNgDoc(documentation, doc) {
if (!doc.ngdoc) return;
console.log('Found:', doc.ngdoc + ':' + doc.name);
documentation.byName[doc.name] = doc;
if (doc.methodOf) {
if (parent = documentation.byName[doc.methodOf]) {
(parent.method = parent.method || []).push(doc);
} else {
throw 'Owner "' + doc.methodOf + '" is not defined.';
}
} else {
documentation.pages.push(doc);
keywordPages.push({
name:doc.name,
type: doc.ngdoc,
keywords:keywords(doc.raw.text)
});
}
}
function writeDoc(pages) {
pages.forEach(function(doc) {
mergeTemplate(
doc.ngdoc + '.template',
doc.name + '.html', doc, callback.chain());
});
}
+58
View File
@@ -0,0 +1,58 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<h3>In HTML Template Binding</h3>
<tt>
<pre>
&lt;{{element}} {{shortName}}="{{paramFirst.name}}"&gt;
...
&lt;/{{element}}&gt;
</pre>
</tt>
<h3>Parameters</h3>
<ul>
{{#param}}
<li><tt>{{name}}</tt>
<tt>&#123;{{#type}}{{type}}{{/type}}{{^type}}*{{/type}}{{#optional}}={{/optional}}&#125;</tt>
<tt>{{#default}}[{{default}}]{{/default}}</tt>
{{{description}}}</li>
{{/param}}
</ul>
{{{paramDescription}}}
{{#css}}
<h3>CSS</h3>
{{{css}}}
{{/css}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+35
View File
@@ -0,0 +1,35 @@
@namespace doc url("http://docs.angularjs.org/");
doc\:example {
display: none;
}
ul.doc-example {
list-style-type: none;
position: relative;
font-size: 14px;
}
ul.doc-example > li {
border: 2px solid gray;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
background-color: white;
margin-bottom: 20px;
}
ul.doc-example > li.doc-example-heading {
border: none;
border-radius: none;
margin-bottom: -10px;
}
li.doc-example-live {
padding: 10px;
font-size: 1.2em;
}
div.syntaxhighlighter {
padding-bottom: 1px !important; /* fix to remove unnecessary scrollbars http://is.gd/gSMgC */
}
+71
View File
@@ -0,0 +1,71 @@
(function(){
var angularJsUrl;
var scripts = document.getElementsByTagName("script");
var filename = /(.*\/)angular([^\/]*)/;
for(var j = 0; j < scripts.length; j++) {
var src = scripts[j].src;
if (src && src.match(filename)) {
angularJsUrl = src;
}
}
var HTML_TEMPLATE =
'<!doctype html>\n' +
'<html xmlns:ng="http://angularjs.org">\n' +
' <script type="text/javascript" ng:autobind\n' +
' src="' + angularJsUrl + '"></script>\n' +
' <body>\n' +
'_HTML_SOURCE_\n' +
' </body>\n' +
'</html>';
angular.widget('doc:example', function(element){
this.descend(true); //compile the example code
element.hide();
var example = element.find('doc\\:source').eq(0),
exampleSrc = example.text(),
scenario = element.find('doc\\:scenario').eq(0);
var code = indent(exampleSrc);
var tabs = angular.element(
'<ul class="doc-example">' +
'<li class="doc-example-heading"><h3>Source</h3></li>' +
'<li class="doc-example-source" ng:non-bindable>' +
'<pre class="brush: js; html-script: true; highlight: [' +
code.hilite + ']; toolbar: false;"></pre></li>' +
'<li class="doc-example-heading"><h3>Live Preview</h3></li>' +
'<li class="doc-example-live">' + exampleSrc +'</li>' +
'<li class="doc-example-heading"><h3>Scenario Test</h3></li>' +
'<li class="doc-example-scenario"><pre class="brush: js">' + scenario.text() + '</pre></li>' +
'</ul>');
tabs.find('li.doc-example-source > pre').text(HTML_TEMPLATE.replace('_HTML_SOURCE_', code.html));
element.html('');
element.append(tabs);
element.show();
var script = (exampleSrc.match(/<script[^\>]*>([\s\S]*)<\/script>/) || [])[1] || '';
try {
eval(script);
} catch (e) {
alert(e);
}
});
function indent(text) {
var lines = text.split(/\n/);
var lineNo = [];
while (lines[0].match(/^\s*$/)) lines.shift();
while (lines[lines.length - 1].match(/^\s*$/)) lines.pop();
for ( var i = 0; i < lines.length; i++) {
lines[i] = ' ' + lines[i];
lineNo.push(6 + i);
}
return {html: lines.join('\n'), hilite: lineNo.join(',') };
};
})();
+1
View File
@@ -0,0 +1 @@
NG_PAGES={{{JSON}}};
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html xmlns:ng="http://angularjs.org" wiki:ng="http://angularjs.org">
<head>
<title>&lt;angular/&gt; Docs Scenario Runner</title>
<script type="text/javascript" src="../angular-scenario.js" ng:autobind></script>
<script type="text/javascript" src="docs-scenario.js"></script>
</head>
<body>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{{#pages}}
describe('{{name}}', function(){
beforeEach(function(){
browser().navigateTo('index.html#!{{name}}');
});
// {{raw.file}}:{{raw.line}}
{{{scenario}}}
});
{{/pages}}
+177
View File
@@ -0,0 +1,177 @@
body {
font-family: Arial, sans-serif;
font-size: 14px;
margin: 0;
padding: 0;
}
#page {
display: table-row;
}
#sidebar,
#section {
display: table-cell;
}
a {
color: blue;
}
.nav-section {
margin-left: 1em;
margin-top: 0.5em;
}
.section-title {
float: right;
}
#header {
background-color: #F2C200;
border-bottom: 1px solid #957800;
}
#header h1 {
font-weight: normal;
font-size: 30px;
line-height: 30px;
margin: 0;
padding: 10px 10px;
height: 30px;
}
#header .angular {
font-family: Courier New, monospace;
font-weight: bold;
}
#header h1 a {
color: black;
text-decoration: none;
}
#header h1 a:hover {
text-decoration: underline;
}
#section {
padding: 1em;
width: 100%;
}
#section h1 {
font-family: monospace;
margin-top: 0;
padding-bottom: 5px;
border-bottom: 1px solid #CCC;
}
#section h2 {
margin-top: 1.8em;
}
#section h1 + h2 {
margin-top: 1.3em;
}
#section h3 {
margin-top: 1.5em;
}
#sidebar {
padding: 10px 10px 20px 10px;
background-color: #EEE;
border-right: 1px solid #DDD;
}
#sidebar a {
text-decoration: none;
}
#sidebar a:hover {
text-decoration: underline;
}
#sidebar input {
width: 175px;
margin-bottom: 1em;
}
#sidebar ul {
list-style-type: none;
/*TODO(esprehn): Can we just reset globally and not break examples?*/
margin: 0;
padding: 0;
}
#sidebar ul li {
}
#sidebar ul li a {
display: block;
padding: 2px 2px 2px 4px;
}
#sidebar ul li.selected a {
background-color: #DDD;
border-radius: 5px;
-moz-border-radius: 5px;
border: 1px solid #CCC;
padding: 1px 1px 1px 3px;
}
#sidebar ul li.level-0 {
margin-left: 0em;
font-weight: bold;
font-size: 1.2em;
}
#sidebar ul li.level-1.level-angular {
font-family: monospace;
font-weight: normal;
font-size: 1em;
margin-top: 0;
margin-bottom: 0;
}
#sidebar ul li.level-1 {
margin-left: 1em;
margin-top: 5px;
font-size: 1.1em;
font-weight: bold;
}
#sidebar ul li.level-2 {
margin-left: 2em;
font-family: monospace;
}
#sidebar ul li.level-3 {
margin-left: 3em;
font-family: monospace;
}
.deprecated {
border: 2px solid red;
}
.deprecated legend {
font-weight: bold;
color: red;
}
.workInProgress {
border: 2px solid orange;
}
.workInProgress legend {
font-weight: bold;
color: orange;
}
#feedback {
float: right;
width: 10em;
text-align: right;
}
+48
View File
@@ -0,0 +1,48 @@
SyntaxHighlighter['defaults'].toolbar = false;
DocsController.$inject = ['$location', '$browser', '$window'];
function DocsController($location, $browser, $window) {
this.pages = NG_PAGES;
window.$root = this.$root;
this.getUrl = function(page){
return '#!' + page.name;
};
this.getCurrentPartial = function(){
return './' + this.getTitle() + '.html';
};
this.getTitle = function(){
var hashPath = $location.hashPath || '!angular';
if (hashPath.match(/^!angular/)) {
this.partialTitle = hashPath.substring(1);
}
return this.partialTitle;
};
this.getClass = function(page) {
var depth = page.name.split(/\./).length - 1,
cssClass = 'level-' + depth + (page.name == this.getTitle() ? ' selected' : '');
if (depth == 1 && page.type !== 'overview') cssClass += ' level-angular';
return cssClass;
};
this.afterPartialLoaded = function() {
$window.scroll(0,0);
SyntaxHighlighter.highlight();
};
this.getFeedbackUrl = function() {
return "mailto:angular@googlegroups.com?" +
"subject=" + escape("Feedback on " + $location.href) + "&" +
"body=" + escape("Hi there,\n\nI read " + $location.href + " and wanted to ask ....");
}
}
angular.filter('short', function(name){
return (name||'').split(/\./).pop();
});
+65
View File
@@ -0,0 +1,65 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<h3>In HTML Template Binding</h3>
<tt>
<span>{{</span>
{{paramFirst.name}}_expression
| {{shortName}}{{#paramRest}}{{^default}}:{{name}}{{/default}}{{#default}}<i>[:{{name}}={{default}}]</i>{{/default}}{{/paramRest}}
<span> }}</span>
</tt>
<h3>In JavaScript</h3>
<tt ng:non-bindable>
angular.filter.{{shortName}}({{paramFirst.name}}{{#paramRest}}, {{name}}{{/paramRest}} );
</tt>
<h3>Parameters</h3>
<ul>
{{#param}}
<li><tt>{{name}}</tt>
<tt>&#123;{{#type}}{{type}}{{/type}}{{^type}}*{{/type}}{{#optional}}={{/optional}}&#125;</tt>
<tt>{{#default}}[{{default}}]{{/default}}</tt>
{{{description}}}</li>
{{/param}}
</ul>
{{#returns}}
<h3>Returns</h3>
<tt>&#123;{{{type}}}&#125;</tt> {{{description}}}
{{/returns}}
{{#css}}
<h3>CSS</h3>
{{{css}}}
{{/css}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+53
View File
@@ -0,0 +1,53 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<h3>In HTML Template Binding</h3>
<tt>
&lt;input type="text" ng:format="{{shortName}}"&gt;
</tt>
<h3>In JavaScript</h3>
<tt ng:non-bindable>
var userInputString = angular.formatter.{{shortName}}.format(modelValue);<br/>
var modelValue = angular.formatter.{{shortName}}.parse(userInputString);
</tt>
{{#returns}}
<h3>Returns</h3>
<tt>&#123;{{{type}}}&#125;</tt> {{{description}}}
{{/returns}}
{{#css}}
<h3>CSS</h3>
{{{css}}}
{{/css}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+52
View File
@@ -0,0 +1,52 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<tt ng:non-bindable>
{{name}}({{paramFirst.name}}{{#paramRest}}, {{name}}{{/paramRest}} );
</tt>
<h3>Parameters</h3>
<ul>
{{#param}}
<li><tt>{{name}}</tt>
<tt>&#123;{{#type}}{{type}}{{/type}}{{^type}}*{{/type}}{{#optional}}={{/optional}}&#125;</tt>
<tt>{{#default}}[{{default}}]{{/default}}</tt>
{{{description}}}</li>
{{/param}}
</ul>
{{#returns}}
<h3>Returns</h3>
<tt>&#123;{{{type}}}&#125;</tt> {{{description}}}
{{/returns}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE HTML>
<html xmlns:ng="http://angularjs.org/" xmlns:doc="http://docs.angularjs.org/" ng:controller="DocsController">
<head>
<title ng:bind-template="&lt;angular/&gt;: {{getTitle()}}">&lt;angular/&gt;</title>
<meta name="fragment" content="!">
<link rel="stylesheet" href="doc_widgets.css" type="text/css" />
<link rel="stylesheet" href="docs.css" type="text/css"/>
<link rel="stylesheet" href="http://alexgorbatchev.com/pub/sh/current/styles/shCore.css" type="text/css"/>
<link rel="stylesheet" href="http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css" type="text/css"/>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js"></script>
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js"></script>
<script type="text/javascript" src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js"></script>
<script type="text/javascript" src="../angular.min.js" ng:autobind></script>
<script type="text/javascript" src="docs.js"></script>
<script type="text/javascript" src="doc_widgets.js"></script>
<script type="text/javascript" src="docs-data.js"></script>
</head>
<body>
<div id="header">
<h1>
<span class="section-title">{{getTitle()}}</span>
<a href="index.html"><span class="angular">&lt;angular/&gt;</span> Docs</a>
</h1>
</div>
<div id="page">
<div id="sidebar" class="nav">
<div class="doc-list">
<input type="text" name="filterText" placeholder="search documentaiton"/>
<ul>
<li ng:repeat="page in pages.$filter(filterText)" ng:class="getClass(page)">
<a href="{{getUrl(page)}}" ng:click="">{{page.name | short}}</a>
</li>
</ul>
</div>
</div>
<div id="section">
<a id="feedback" ng:href="{{getFeedbackUrl()}}">Report an Issue or Ask a Question</a>
<ng:include src="getCurrentPartial()" onload="afterPartialLoaded()"></ng:include>
</div>
</div>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
{{{description}}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+53
View File
@@ -0,0 +1,53 @@
<h1><tt>{{name}}</tt></h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Dependencies</h2>
<ul>
{{#requires}}
<li><tt>{{name}}</tt></li>
{{/requires}}
</ul>
<h2>Methods</h2>
<ul>
{{#method}}
<li><tt>{{shortName}}</tt>: {{{description}}}</li>
{{/method}}
</ul>
<h2>Properties</h2>
<ul>
{{#property}}
<li><tt>{{name}}:{{#type}}{{type}}{{/type}}</tt>{{#description}}: {{{description}}}{{/description}}</li>
{{/property}}
</ul>
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
{{#scenario}}<doc:scenario>{{{scenario}}}</doc:scenario>{{/scenario}}
</doc:example>
{{/example}}
+277
View File
@@ -0,0 +1,277 @@
console.log(__dirname);
require.paths.push(__dirname + "/../");
require.paths.push(__dirname + "/../../");
var fs = require('fs');
var Script = process.binding('evals').Script;
var collect = load('docs/collect.js');
describe('collect', function(){
describe('markdown', function(){
it('should replace angular in markdown', function(){
expect(collect.markdown('<angular/>')).
toEqual('<p><tt>&lt;angular/&gt;</tt></p>');
});
it('should not replace anything in <pre>', function(){
expect(collect.markdown('bah x\n<pre>\nangular.k\n</pre>\n asdf x')).
toEqual(
'<p>bah x</p>' +
'<pre>\nangular.k\n</pre>' +
'<p>asdf x</p>');
});
});
describe('processNgDoc', function() {
var processNgDoc = collect.processNgDoc,
documentation;
beforeEach(function() {
documentation = {
pages: [],
byName: {}
};
});
it('should store references to docs by name', function() {
var doc = {ngdoc: 'section', name: 'fake', raw: {text:''}};
processNgDoc(documentation, doc);
expect(documentation.byName.fake).toBe(doc);
});
it('should connect doc to owner (specified by @methodOf)', function() {
var parentDoc = {ngdoc: 'section', name: 'parent', raw: {text:''}};
var doc = {ngdoc: 'section', name: 'child', methodOf: 'parent', raw: {text:''}};
processNgDoc(documentation, parentDoc);
processNgDoc(documentation, doc);
expect(documentation.byName.parent.method).toBeDefined();
expect(documentation.byName.parent.method[0]).toBe(doc);
});
it('should not add doc to sections if @memberOf specified', function() {
var parentDoc = {ngdoc: 'parent', name: 'parent', raw: {text:''}};
var doc = {ngdoc: 'child', name: 'child', methodOf: 'parent', raw: {text:''}};
processNgDoc(documentation, parentDoc);
processNgDoc(documentation, doc);
expect(documentation.pages.child).not.toBeDefined();
});
it('should throw exception if owner does not exist', function() {
expect(function() {
processNgDoc(documentation, {ngdoc: 'section', methodOf: 'not.exist', raw: {text:''}});
}).toThrow('Owner "not.exist" is not defined.');
});
it('should ignore non-ng docs', function() {
var doc = {name: 'anything'};
expect(function() {
processNgDoc(documentation, doc);
}).not.toThrow();
expect(documentation.pages).not.toContain(doc);
});
});
describe('TAG', function(){
var TAG = collect.TAG;
var doc;
beforeEach(function(){
doc = {};
});
describe('@param', function(){
it('should parse with no default', function(){
TAG.param(doc, 'param',
'{(number|string)} number Number \n to format.');
expect(doc.param).toEqual([{
type : '(number|string)',
name : 'number',
optional: false,
'default' : undefined,
description : 'Number \n to format.' }]);
});
it('should parse with default and optional', function(){
TAG.param(doc, 'param',
'{(number|string)=} [fractionSize=2] desc');
expect(doc.param).toEqual([{
type : '(number|string)',
name : 'fractionSize',
optional: true,
'default' : '2',
description : 'desc' }]);
});
});
describe('@requires', function() {
it('should parse more @requires tag into array', function() {
TAG.requires(doc, 'requires', '$service');
TAG.requires(doc, 'requires', '$another');
expect(doc.requires).toEqual([
{name: '$service'},
{name: '$another'}
]);
});
});
describe('@property', function() {
it('should parse @property tags into array', function() {
TAG.property(doc, 'property', '{type} name1 desc');
TAG.property(doc, 'property', '{type} name2 desc');
expect(doc.property.length).toEqual(2);
});
it('should parse @property with only name', function() {
TAG.property(doc, 'property', 'fake');
expect(doc.property[0].name).toEqual('fake');
});
it('should parse @property with optional type', function() {
TAG.property(doc, 'property', '{string} name');
expect(doc.property[0].name).toEqual('name');
expect(doc.property[0].type).toEqual('string');
});
it('should parse @property with optional description', function() {
TAG.property(doc, 'property', 'name desc rip tion');
expect(doc.property[0].name).toEqual('name');
expect(doc.property[0].description).toEqual('desc rip tion');
});
it('should parse @property with type and description both', function() {
TAG.property(doc, 'property', '{bool} name desc rip tion');
expect(doc.property[0].name).toEqual('name');
expect(doc.property[0].type).toEqual('bool');
expect(doc.property[0].description).toEqual('desc rip tion');
});
/**
* If property description is undefined, this variable is not set in the template,
* so the whole @description tag is used instead
*/
it('should set undefined description to "false"', function() {
TAG.property(doc, 'property', 'name');
expect(doc.property[0].description).toBe(false);
});
});
describe('@methodOf', function() {
it('should parse @methodOf tag', function() {
expect(function() {
TAG.methodOf(doc, 'methodOf', 'parentName');
}).not.toThrow();
expect(doc.methodOf).toEqual('parentName');
});
});
describe('@returns', function() {
it('should not parse @returns without type', function() {
expect(function() {TAG.returns(doc, 'returns', 'lala');})
.toThrow();
});
it('should parse @returns with type and description', function() {
TAG.returns(doc, 'returns', '{string} descrip tion');
expect(doc.returns).toEqual({type: 'string', description: 'descrip tion'});
});
it('should transform description of @returns with markdown', function() {
TAG.returns(doc, 'returns', '{string} descrip *tion*');
expect(doc.returns).toEqual({type: 'string', description: 'descrip <em>tion</em>'});
});
});
describe('@description', function(){
it('should support pre blocks', function(){
TAG.description(doc, 'description', '<pre>abc</pre>');
expect(doc.description).
toBe('<div ng:non-bindable><pre class="brush: js; html-script: true;">abc</pre></div>');
});
it('should support multiple pre blocks', function() {
TAG.description(doc, 'description', 'foo \n<pre>abc</pre>\n#bah\nfoo \n<pre>cba</pre>');
expect(doc.description).
toBe('<p>foo </p>' +
'<div ng:non-bindable><pre class="brush: js; html-script: true;">abc</pre></div>' +
'<h2>bah</h2>\n\n' +
'<p>foo </p>' +
'<div ng:non-bindable><pre class="brush: js; html-script: true;">cba</pre></div>');
});
it('should support nested @link annotations with or without description', function() {
TAG.description(doc, 'description',
'foo {@link angular.foo}\n\n da {@link angular.foo bar foo bar } \n\n' +
'dad{@link angular.foo}\n\n' +
'{@link angular.directive.ng:foo ng:foo}');
expect(doc.description).
toBe('<p>foo <a href="#!angular.foo"><code>angular.foo</code></a></p>\n\n' +
'<p>da <a href="#!angular.foo"><code>bar foo bar</code></a> </p>\n\n' +
'<p>dad<a href="#!angular.foo"><code>angular.foo</code></a></p>\n\n' +
'<p><a href="#!angular.directive.ng:foo"><code>ng:foo</code></a></p>');
});
it('should increment all headings by one', function() {
TAG.description(doc, 'description', '# foo\nabc');
expect(doc.description).
toBe('<h2>foo</h2>\n\n<p>abc</p>');
});
});
describe('@example', function(){
it('should not remove {{}}', function(){
TAG.example(doc, 'example', 'text {{ abc }}');
expect(doc.example).toEqual('text {{ abc }}');
});
});
describe('@deprecated', function() {
it('should parse @deprecated', function() {
TAG.deprecated(doc, 'deprecated', 'Replaced with foo.');
expect(doc.deprecated).toBe('Replaced with foo.');
})
});
describe('@workInProgress', function() {
it('should parse @workInProgress without a description and default to true', function() {
TAG.workInProgress(doc, 'workInProgress', '');
expect(doc.workInProgress).toEqual({description: ''});
});
it('should parse @workInProgress with a description', function() {
TAG.workInProgress(doc, 'workInProgress', 'my description');
expect(doc.workInProgress).toEqual({description: '<p>my description</p>'});
});
});
});
describe('trim', function(){
var trim = collect.trim;
it('should remove leading/trailing space', function(){
expect(trim(' \nabc\n ')).toEqual('abc');
});
it('should remove leading space on every line', function(){
expect(trim('\n 1\n 2\n 3\n')).toEqual('1\n 2\n 3');
});
});
describe('keywords', function(){
var keywords = collect.keywords;
it('should collect keywords', function(){
expect(keywords('\nHello: World! @ignore.')).toEqual('hello world');
expect(keywords('The `ng:class-odd` and ')).toEqual('and ng:class-odd the');
});
});
});
function load(path){
var sandbox = {
require: require,
console: console,
__dirname: __dirname,
testmode: true
};
Script.runInNewContext(fs.readFileSync(path), sandbox, path);
return sandbox;
}
+21
View File
@@ -0,0 +1,21 @@
require.paths.push("./lib");
var jasmine = require('jasmine-1.0.1');
var sys = require('util');
for(var key in jasmine) {
global[key] = jasmine[key];
}
var isVerbose = false;
var showColors = true;
process.argv.forEach(function(arg){
switch(arg) {
case '--color': showColors = true; break;
case '--noColor': showColors = false; break;
case '--verbose': isVerbose = true; break;
}
});
jasmine.executeSpecsInFolder(__dirname + '/spec', function(runner, log){
process.exit(runner.results().failedCount);
}, isVerbose, showColors);
+59
View File
@@ -0,0 +1,59 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<h3>In HTML Template Binding</h3>
<tt>
&lt;input type="text" ng:validate="{{shortName}}{{#paramRest}}{{^default}}:{{name}}{{/default}}{{#default}}<i>[:{{name}}]</i>{{/default}}{{/paramRest}}"/>
</tt>
<h3>In JavaScript</h3>
<tt ng:non-bindable>
angular.validator.{{shortName}}({{paramFirst.name}}{{#paramRest}}{{^default}}, {{name}}{{/default}}{{#default}}<i>[, {{name}}]</i>{{/default}}{{/paramRest}} );
</tt>
<h3>Parameters</h3>
<ul>
{{#param}}
<li><tt>{{name}}</tt>
<tt>&#123;{{#type}}{{type}}{{/type}}{{^type}}*{{/type}}{{#optional}}={{/optional}}&#125;</tt>
<tt>{{#default}}[{{default}}]{{/default}}</tt>
{{{description}}}</li>
{{/param}}
</ul>
{{{paramDescription}}}
{{#css}}
<h3>CSS</h3>
{{{css}}}
{{/css}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+68
View File
@@ -0,0 +1,68 @@
<h1>{{name}}</h1>
{{#workInProgress}}
<fieldset class="workInProgress">
<legend>Work In Progress</legend>
This page is currently being revised. It might be incomplete or contain inaccuracies.
{{{workInProgress.description}}}
</fieldset>
{{/workInProgress}}
{{#deprecated}}
<fieldset class="deprecated">
<legend>Deprecated API</legend>
{{deprecated}}
</fieldset>
{{/deprecated}}
<h2>Description</h2>
{{{description}}}
<h2>Usage</h2>
<h3>In HTML Template Binding</h3>
<tt>
{{^element}}
<pre>
&lt;{{shortName}}{{#param}} {{#default}}<i>[</i>{{/default}}{{name}}="..."{{#default}}<i>]</i>{{/default}}{{/param}}&gt;{{#usageContent}}
{{usageContent}}
{{/usageContent}}&lt;/{{shortName}}&gt;
</pre>
{{/element}}
{{#element}}
<pre>
&lt;{{element}} {{shortName}}{{#paramFirst}}="{{paramFirst.name}}{{/paramFirst}}"&gt;
...
&lt;/{{element}}&gt;
</pre>
{{/element}}
</tt>
<h3>Parameters</h3>
<ul>
{{#param}}
<li><tt>{{name}}</tt>
<tt>&#123;{{#type}}{{type}}{{/type}}{{^type}}*{{/type}}{{#optional}}={{/optional}}&#125;</tt>
<tt>{{#default}}[{{default}}]{{/default}}</tt>
{{{description}}}</li>
{{/param}}
</ul>
{{{paramDescription}}}
{{#css}}
<h3>CSS</h3>
{{{css}}}
{{/css}}
{{#example}}
<h2>Example</h2>
{{{exampleDescription}}}
<doc:example>
<doc:source>
{{/example}}
{{{example}}}
{{#example}}
</doc:source>
<doc:scenario>{{{scenario}}}</doc:scenario>
</doc:example>
{{/example}}
+3 -3
View File
@@ -23,7 +23,7 @@
<ul>
<li ng:repeat="item in activities.data.items.$filter(filterText)">
<h1>
<img src="{{item.actor.thumbnailUrl}}"/>
<img ng:src="{{item.actor.thumbnailUrl}}"/>
<a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
<a href="#" ng:click="expandReplies(item)">Replies: {{item.links.replies[0].count}}</a>
</h1>
@@ -31,14 +31,14 @@
{{item.object.content | html}}
<div>
<a href="{{attachment.links.enclosure[0].href}}" ng:repeat="attachment in item.object.attachments">
<img src="{{attachment.links.preview[0].href}}"/>
<img ng:src="{{attachment.links.preview[0].href}}"/>
</a>
</div>
</div>
<my:expand expand="item.replies.show">
<ul>
<li ng:repeat="reply in item.replies.data.items">
<img src="{{reply.actor.thumbnailUrl}}"/>
<img ng:src="{{reply.actor.thumbnailUrl}}"/>
<a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>:
{{reply.content | html}}
</li>
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html xmlns:ng="http://angularjs.org">
<head>
<title>Personal Log</title>
<script type="text/javascript" src="../../src/angular-bootstrap.js" ng:autobind></script>
<script type="text/javascript" src="personalLog.js"></script>
</head>
<!-- TODO: we need to expose $root so that we can delete cookies in the scenario runner, there
must be a better way to do this -->
<body ng:controller="example.personalLog.LogCtrl" ng:init="$window.$root = $root">
<form action="" ng:submit="addLog(newMsg)">
<input type="text" name="newMsg" />
<input type="submit" value="add" />
<input type="button" value="remove all" ng:click="rmLogs()" />
</form>
<hr/>
<h2>Logs:</h2>
<ul>
<li ng:repeat="log in logs.$orderBy('-at')">
{{log.at | date:'yy-MM-dd HH:mm'}} {{log.msg}}
[<a href="" ng:click="rmLog(log)">x</a>]
</li>
</ul>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
/**
* @fileOverview Very simple personal log demo application to demonstrate angular functionality,
* especially:
* - the MVC model
* - testability of controllers
* - dependency injection for controllers via $inject and constructor function
* - $cookieStore for persistent cookie-backed storage
* - simple templating constructs such as ng:repeat and {{}}
* - date filter
* - and binding onSubmit and onClick events to angular expressions
* @author Igor Minar
*/
/** @namespace the 'example' namespace */
var example = example || {};
/** @namespace namespace of the personal log app */
example.personalLog = {};
//name space isolating closure
(function() {
var LOGS = 'logs';
/**
* The controller for the personal log app.
*/
function LogCtrl($cookieStore) {
var self = this,
logs = self.logs = $cookieStore.get(LOGS) || []; //main model
/**
* Adds newMsg to the logs array as a log, persists it and clears newMsg.
* @param {string} msg Message to add (message is passed as parameter to make testing easier).
*/
this.addLog = function(msg) {
var newMsg = msg || self.newMsg;
if (!newMsg) return;
var log = {
at: new Date().getTime(),
msg: newMsg
}
logs.push(log);
$cookieStore.put(LOGS, logs);
self.newMsg = '';
};
/**
* Persistently removes a log from logs.
* @param {object} log The log to remove.
*/
this.rmLog = function(log) {
angular.Array.remove(logs, log);
$cookieStore.put(LOGS, logs);
};
/**
* Persistently removes all logs.
*/
this.rmLogs = function() {
logs.splice(0, logs.length);
$cookieStore.remove(LOGS);
};
}
//inject
LogCtrl.$inject = ['$cookieStore'];
//export
example.personalLog.LogCtrl = LogCtrl;
})();
@@ -0,0 +1,97 @@
describe('personal log', function() {
beforeEach(function() {
browser().navigateTo('../personalLog.html');
});
afterEach(function() {
clearCookies();
});
it('should create new logs and order them in reverse chronological order', function(){
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(1);
expect(repeater('ul li').column('log.msg')).toEqual('my first message');
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
expect(repeater('ul li').column('log.msg')).toEqual(['my second message', 'my first message']);
});
it('should delete a log when user clicks on the related X link', function() {
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
element('ul li a:eq(1)').click();
expect(repeater('ul li').count()).toEqual(1);
expect(repeater('ul li').column('log.msg')).toEqual('my second message');
element('ul li a:eq(0)').click();
expect(repeater('ul li').count()).toEqual(0);
});
it('should delete all cookies when user clicks on "remove all" button', function() {
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
element('input[value="remove all"]').click();
expect(repeater('ul li').count()).toEqual(0);
});
it('should preserve logs over page reloads', function() {
input('newMsg').enter('my persistent message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(1);
browser().reload();
expect(repeater('ul li').column('log.msg')).toEqual('my persistent message');
expect(repeater('ul li').count()).toEqual(1);
});
});
/**
* DSL for deleting all cookies.
*/
angular.scenario.dsl('clearCookies', function() {
/**
* Deletes cookies by interacting with the cookie service within the application under test.
*/
return function() {
this.addFutureAction('clear all cookies', function($window, $document, done) {
//TODO: accessing angular services is pretty nasty, we need a better way to reach them
var $cookies = $window.$root.$cookies,
cookieName;
for (cookieName in $cookies) {
console.log('deleting cookie: ' + cookieName);
delete $cookies[cookieName];
}
$window.$root.$eval();
done();
});
};
});
+10
View File
@@ -0,0 +1,10 @@
<!doctype html">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Personal Log Scenario Runner</title>
<script type="text/javascript" src="../../../src/scenario/angular-bootstrap.js"></script>
<script type="text/javascript" src="personalLogScenario.js"></script>
</head>
<body>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
describe('example.personalLog.LogCtrl', function() {
var logCtrl;
function createNotesCtrl() {
var scope = angular.scope();
return scope.$new(example.personalLog.LogCtrl);
}
beforeEach(function() {
logCtrl = createNotesCtrl();
});
it('should initialize notes with an empty array', function() {
expect(logCtrl.logs).toEqual([]);
});
describe('addLog', function() {
beforeEach(function() {
expect(logCtrl.logs).toEqual([]);
});
it('should add newMsg to logs as a log entry', function() {
logCtrl.newMsg = 'first log message';
logCtrl.addLog();
expect(logCtrl.logs.length).toBe(1);
expect(logCtrl.logs[0].msg).toBe('first log message');
//one more msg, this time passed in as param
logCtrl.addLog('second log message');
expect(logCtrl.logs.length).toBe(2);
expect(logCtrl.logs[0].msg).toBe('first log message');
expect(logCtrl.logs[1].msg).toBe('second log message');
});
it('should clear newMsg when log entry is persisted', function() {
logCtrl.addLog('first log message');
expect(logCtrl.newMsg).toBe('');
});
it('should store logs in the logs cookie', function() {
expect(logCtrl.$cookies.logs).not.toBeDefined();
logCtrl.addLog('first log message');
expect(logCtrl.$cookies.logs).toBeTruthy();
});
it('should do nothing if newMsg is empty', function() {
logCtrl.addLog('');
expect(logCtrl.logs.length).toBe(0);
});
});
describe('rmLog', function() {
beforeEach(function() {
logCtrl.addLog('message1');
logCtrl.addLog('message2');
logCtrl.addLog('message3');
logCtrl.addLog('message4');
expect(logCtrl.logs.length).toBe(4);
});
it('should delete a message identified by index', function() {
logCtrl.rmLog(logCtrl.logs[1]);
expect(logCtrl.logs.length).toBe(3);
logCtrl.rmLog(logCtrl.logs[2]);
expect(logCtrl.logs.length).toBe(2);
expect(logCtrl.logs[0].msg).toBe('message1');
expect(logCtrl.logs[1].msg).toBe('message3');
});
it('should update cookies when a log is deleted', function() {
expect(logCtrl.$cookies.logs).toMatch(/\[\{.*?\}(,\{.*?\}){3}\]/);
logCtrl.rmLog(logCtrl.logs[1]);
expect(logCtrl.$cookies.logs).toMatch(/\[\{.*?\}(,\{.*?\}){2}\]/);
logCtrl.rmLog(logCtrl.logs[0]);
logCtrl.rmLog(logCtrl.logs[0]);
logCtrl.rmLog(logCtrl.logs[0]);
expect(logCtrl.$cookies.logs).toMatch(/\[\]/);
});
});
describe('rmLogs', function() {
beforeEach(function() {
logCtrl.addLog('message1');
logCtrl.addLog('message2');
logCtrl.addLog('message3');
logCtrl.addLog('message4');
expect(logCtrl.logs.length).toBe(4);
});
it('should remove all logs', function() {
logCtrl.rmLogs();
expect(logCtrl.logs).toEqual([]);
});
it('should remove logs cookie', function() {
expect(logCtrl.$cookies.logs).toBeTruthy();
logCtrl.rmLogs();
expect(logCtrl.$cookies.logs).not.toBeDefined();
});
});
});
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
node docs/specs.js --noColor && node docs/collect.js
+34
View File
@@ -0,0 +1,34 @@
server: http://localhost:9876
load:
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_remove.js
- src/Angular.js
- src/JSON.js
- src/*.js
- example/personalLog/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
- src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
- test/scenario/output/*.js
- test/*.js
- example/personalLog/test/*.js
exclude:
- test/jquery_alias.js
- src/angular.prefix
- src/angular.suffix
- src/angular-bootstrap.js
- src/scenario/angular-bootstrap.js
- src/AngularPublic.js
plugin:
- name: "coverage"
jar: "lib/jstestdriver/coverage.jar"
module: "com.google.jstestdriver.coverage.CoverageModule"
+4 -2
View File
@@ -1,7 +1,7 @@
server: http://localhost:9876
load:
- lib/jasmine/jasmine-1.0.1.js
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_alias.js
@@ -10,9 +10,11 @@ load:
- src/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
- src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
- test/scenario/output/*.js
- test/*.js
exclude:
@@ -20,6 +22,6 @@ exclude:
- src/angular.suffix
- src/angular-bootstrap.js
- src/AngularPublic.js
- src/scenario/bootstrap.js
- src/scenario/angular-bootstrap.js
- test/jquery_remove.js
+6 -2
View File
@@ -1,24 +1,28 @@
server: http://localhost:9876
load:
- lib/jasmine/jasmine-1.0.1.js
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_remove.js
- src/Angular.js
- src/JSON.js
- src/*.js
- example/personalLog/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
- src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
- test/scenario/output/*.js
- test/*.js
- example/personalLog/test/*.js
exclude:
- test/jquery_alias.js
- src/angular.prefix
- src/angular.suffix
- src/angular-bootstrap.js
- src/scenario/bootstrap.js
- src/scenario/angular-bootstrap.js
- src/AngularPublic.js
+309
View File
@@ -0,0 +1,309 @@
/*
* HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* // or to get an XML string:
* HTMLtoXML(htmlString);
*
* // or to get an XML DOM Document
* HTMLtoDOM(htmlString);
*
* // or to inject into an existing document/DOM node
* HTMLtoDOM(htmlString, document);
* HTMLtoDOM(htmlString, document.body);
*
*/
(function(){
// Regular Expressions for parsing tags and attributes
var startTag = /^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag = /^<\/(\w+)[^>]*>/,
attr = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
// Empty Elements - HTML 4.01
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");
// Block Elements - HTML 4.01
var block = makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
// Inline Elements - HTML 4.01
var inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var special = makeMap("script,style");
var htmlParser = this.htmlParser = function( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function(){
return this[ this.length - 1 ];
};
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !special[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") == 0 ) {
index = html.indexOf("-->");
if ( index >= 0 ) {
if ( handler.comment )
handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// end tag
} else if ( html.indexOf("</") == 0 ) {
match = html.match( endTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( endTag, parseEndTag );
chars = false;
}
// start tag
} else if ( html.indexOf("<") == 0 ) {
match = html.match( startTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( startTag, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if ( handler.chars )
handler.chars( text );
}
} else {
html = html.replace(new RegExp("(.*)<\/" + stack.last() + "[^>]*>"), function(all, text){
text = text.replace(/<!--(.*?)-->/g, "$1")
.replace(/<!\[CDATA\[(.*?)]]>/g, "$1");
if ( handler.chars )
handler.chars( text );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last )
throw "Parse Error: " + html;
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
if ( block[ tagName ] ) {
while ( stack.last() && inline[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( closeSelf[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = empty[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
if ( handler.start ) {
var attrs = [];
rest.replace(attr, function(match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs.push({
name: name,
value: value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
});
if ( handler.start )
handler.start( tagName, attrs, unary );
}
}
function parseEndTag( tag, tagName ) {
// If no tag name is provided, clean shop
if ( !tagName )
var pos = 0;
// Find the closest opened tag of the same type
else
for ( var pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( var i = stack.length - 1; i >= pos; i-- )
if ( handler.end )
handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
};
this.HTMLtoXML = function( html ) {
var results = "";
htmlParser(html, {
start: function( tag, attrs, unary ) {
results += "<" + tag;
for ( var i = 0; i < attrs.length; i++ )
results += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
results += (unary ? "/" : "") + ">";
},
end: function( tag ) {
results += "</" + tag + ">";
},
chars: function( text ) {
results += text;
},
comment: function( text ) {
results += "<!--" + text + "-->";
}
});
return results;
};
this.HTMLtoDOM = function( html, doc ) {
// There can be only one of these elements
var one = makeMap("html,head,body,title");
// Enforce a structure for the document
var structure = {
link: "head",
base: "head"
};
if ( !doc ) {
if ( typeof DOMDocument != "undefined" )
doc = new DOMDocument();
else if ( typeof document != "undefined" && document.implementation && document.implementation.createDocument )
doc = document.implementation.createDocument("", "", null);
else if ( typeof ActiveX != "undefined" )
doc = new ActiveXObject("Msxml.DOMDocument");
} else
doc = doc.ownerDocument ||
doc.getOwnerDocument && doc.getOwnerDocument() ||
doc;
var elems = [],
documentElement = doc.documentElement ||
doc.getDocumentElement && doc.getDocumentElement();
// If we're dealing with an empty document then we
// need to pre-populate it with the HTML document structure
if ( !documentElement && doc.createElement ) (function(){
var html = doc.createElement("html");
var head = doc.createElement("head");
head.appendChild( doc.createElement("title") );
html.appendChild( head );
html.appendChild( doc.createElement("body") );
doc.appendChild( html );
})();
// Find all the unique elements
if ( doc.getElementsByTagName )
for ( var i in one )
one[ i ] = doc.getElementsByTagName( i )[0];
// If we're working with a document, inject contents into
// the body element
var curParentNode = one.body;
htmlParser( html, {
start: function( tagName, attrs, unary ) {
// If it's a pre-built element, then we can ignore
// its construction
if ( one[ tagName ] ) {
curParentNode = one[ tagName ];
return;
}
var elem = doc.createElement( tagName );
for ( var attr in attrs )
elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value );
if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] != "boolean" )
one[ structure[ tagName ] ].appendChild( elem );
else if ( curParentNode && curParentNode.appendChild )
curParentNode.appendChild( elem );
if ( !unary ) {
elems.push( elem );
curParentNode = elem;
}
},
end: function( tag ) {
elems.length -= 1;
// Init the new parentNode
curParentNode = elems[ elems.length - 1 ];
},
chars: function( text ) {
curParentNode.appendChild( doc.createTextNode( text ) );
},
comment: function( text ) {
// create comment node
}
});
return doc;
};
function makeMap(str){
var obj = {}, items = str.split(",");
for ( var i = 0; i < items.length; i++ )
obj[ items[i] ] = true;
return obj;
}
})();
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2008-2010 Pivotal Labs
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.
+180
View File
@@ -0,0 +1,180 @@
var fs = require('fs');
var sys = require('sys');
var path = require('path');
var filename = __dirname + '/jasmine.js';
global.window = {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval
};
var src = fs.readFileSync(filename);
var jasmine = process.compile(src + '\njasmine;', filename);
delete global.window;
function noop(){}
jasmine.executeSpecsInFolder = function(folder, done, isVerbose, showColors, matcher){
var log = [];
var columnCounter = 0;
var start = 0;
var elapsed = 0;
var verbose = isVerbose || false;
var fileMatcher = new RegExp(matcher || "\.js$");
var colors = showColors || false;
var specs = jasmine.getAllSpecFiles(folder, fileMatcher);
var ansi = {
green: '\033[32m',
red: '\033[31m',
yellow: '\033[33m',
none: '\033[0m'
};
for (var i = 0, len = specs.length; i < len; ++i){
var filename = specs[i];
require(filename.replace(/\.*$/, ""));
}
var jasmineEnv = jasmine.getEnv();
jasmineEnv.reporter = {
log: function(str){
},
reportSpecStarting: function(runner) {
},
reportRunnerStarting: function(runner) {
sys.puts('Started');
start = Number(new Date);
},
reportSuiteResults: function(suite) {
var specResults = suite.results();
var path = [];
while(suite) {
path.unshift(suite.description);
suite = suite.parentSuite;
}
var description = path.join(' ');
if (verbose)
log.push('Spec ' + description);
specResults.items_.forEach(function(spec){
if (spec.failedCount > 0 && spec.description) {
if (!verbose)
log.push(description);
log.push(' it ' + spec.description);
spec.items_.forEach(function(result){
log.push(' ' + result.trace.stack + '\n');
});
}
});
},
reportSpecResults: function(spec) {
var result = spec.results();
var msg = '';
if (result.passed())
{
msg = (colors) ? (ansi.green + '.' + ansi.none) : '.';
// } else if (result.skipped) { TODO: Research why "result.skipped" returns false when "xit" is called on a spec?
// msg = (colors) ? (ansi.yellow + '*' + ansi.none) : '*';
} else {
msg = (colors) ? (ansi.red + 'F' + ansi.none) : 'F';
}
sys.print(msg);
if (columnCounter++ < 50) return;
columnCounter = 0;
sys.print('\n');
},
reportRunnerResults: function(runner) {
elapsed = (Number(new Date) - start) / 1000;
sys.puts('\n');
log.forEach(function(log){
sys.puts(log);
});
sys.puts('Finished in ' + elapsed + ' seconds');
var summary = jasmine.printRunnerResults(runner);
if(colors)
{
if(runner.results().failedCount === 0 )
sys.puts(ansi.green + summary + ansi.none);
else
sys.puts(ansi.red + summary + ansi.none);
} else {
sys.puts(summary);
}
(done||noop)(runner, log);
}
};
jasmineEnv.execute();
};
jasmine.getAllSpecFiles = function(dir, matcher){
var specs = [];
if (fs.statSync(dir).isFile() && dir.match(matcher)) {
specs.push(dir);
} else {
var files = fs.readdirSync(dir);
for (var i = 0, len = files.length; i < len; ++i){
var filename = dir + '/' + files[i];
if (fs.statSync(filename).isFile() && filename.match(matcher)){
specs.push(filename);
}else if (fs.statSync(filename).isDirectory()){
var subfiles = this.getAllSpecFiles(filename, matcher);
subfiles.forEach(function(result){
specs.push(result);
});
}
}
}
return specs;
};
jasmine.printRunnerResults = function(runner){
var results = runner.results();
var suites = runner.suites();
var msg = '';
msg += suites.length + ' test' + ((suites.length === 1) ? '' : 's') + ', ';
msg += results.totalCount + ' assertion' + ((results.totalCount === 1) ? '' : 's') + ', ';
msg += results.failedCount + ' failure' + ((results.failedCount === 1) ? '' : 's') + '\n';
return msg;
};
function now(){
return new Date().getTime();
}
jasmine.asyncSpecWait = function(){
var wait = jasmine.asyncSpecWait;
wait.start = now();
wait.done = false;
(function innerWait(){
waits(10);
runs(function() {
if (wait.start + wait.timeout < now()) {
expect('timeout waiting for spec').toBeNull();
} else if (wait.done) {
wait.done = false;
} else {
innerWait();
}
});
})();
};
jasmine.asyncSpecWait.timeout = 4 * 1000;
jasmine.asyncSpecDone = function(){
jasmine.asyncSpecWait.done = true;
};
for ( var key in jasmine) {
exports[key] = jasmine[key];
}
+188
View File
@@ -0,0 +1,188 @@
jasmine.TrivialReporter = function(doc) {
this.document = doc || document;
this.suiteDivs = {};
this.logRunningSpecs = false;
};
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
var el = document.createElement(type);
for (var i = 2; i < arguments.length; i++) {
var child = arguments[i];
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
if (child) { el.appendChild(child); }
}
}
for (var attr in attrs) {
if (attr == "className") {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
};
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
var showPassed, showSkipped;
this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
this.createDom('div', { className: 'banner' },
this.createDom('div', { className: 'logo' },
this.createDom('a', { href: 'http://pivotal.github.com/jasmine/', target: "_blank" }, "Jasmine"),
this.createDom('span', { className: 'version' }, runner.env.versionString())),
this.createDom('div', { className: 'options' },
"Show ",
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
)
),
this.runnerDiv = this.createDom('div', { className: 'runner running' },
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
);
this.document.body.appendChild(this.outerDiv);
var suites = runner.suites();
for (var i = 0; i < suites.length; i++) {
var suite = suites[i];
var suiteDiv = this.createDom('div', { className: 'suite' },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
this.suiteDivs[suite.id] = suiteDiv;
var parentDiv = this.outerDiv;
if (suite.parentSuite) {
parentDiv = this.suiteDivs[suite.parentSuite.id];
}
parentDiv.appendChild(suiteDiv);
}
this.startedAt = new Date();
var self = this;
showPassed.onclick = function(evt) {
if (showPassed.checked) {
self.outerDiv.className += ' show-passed';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
}
};
showSkipped.onclick = function(evt) {
if (showSkipped.checked) {
self.outerDiv.className += ' show-skipped';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
}
};
};
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
var results = runner.results();
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
this.runnerDiv.setAttribute("class", className);
//do it twice for IE
this.runnerDiv.setAttribute("className", className);
var specs = runner.specs();
var specCount = 0;
for (var i = 0; i < specs.length; i++) {
if (this.specFilter(specs[i])) {
specCount++;
}
}
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
};
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
var results = suite.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.totalCount == 0) { // todo: change this to check results.skipped
status = 'skipped';
}
this.suiteDivs[suite.id].className += " " + status;
};
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
if (this.logRunningSpecs) {
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
}
};
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
var results = spec.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.skipped) {
status = 'skipped';
}
var specDiv = this.createDom('div', { className: 'spec ' + status },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
this.createDom('a', {
className: 'description',
href: '?spec=' + encodeURIComponent(spec.getFullName()),
title: spec.getFullName()
}, spec.description));
var resultItems = results.getItems();
var messagesDiv = this.createDom('div', { className: 'messages' });
for (var i = 0; i < resultItems.length; i++) {
var result = resultItems[i];
if (result.type == 'log') {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
} else if (result.type == 'expect' && result.passed && !result.passed()) {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
if (result.trace.stack) {
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
}
}
}
if (messagesDiv.childNodes.length > 0) {
specDiv.appendChild(messagesDiv);
}
this.suiteDivs[spec.suite.id].appendChild(specDiv);
};
jasmine.TrivialReporter.prototype.log = function() {
var console = jasmine.getGlobal().console;
if (console && console.log) {
if (console.log.apply) {
console.log.apply(console, arguments);
} else {
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
}
}
};
jasmine.TrivialReporter.prototype.getLocation = function() {
return this.document.location;
};
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
var paramMap = {};
var params = this.getLocation().search.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
}
if (!paramMap["spec"]) return true;
return spec.getFullName().indexOf(paramMap["spec"]) == 0;
};
+166
View File
@@ -0,0 +1,166 @@
body {
font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
}
.jasmine_reporter a:visited, .jasmine_reporter a {
color: #303;
}
.jasmine_reporter a:hover, .jasmine_reporter a:active {
color: blue;
}
.run_spec {
float:right;
padding-right: 5px;
font-size: .8em;
text-decoration: none;
}
.jasmine_reporter {
margin: 0 5px;
}
.banner {
color: #303;
background-color: #fef;
padding: 5px;
}
.logo {
float: left;
font-size: 1.1em;
padding-left: 5px;
}
.logo .version {
font-size: .6em;
padding-left: 1em;
}
.runner.running {
background-color: yellow;
}
.options {
text-align: right;
font-size: .8em;
}
.suite {
border: 1px outset gray;
margin: 5px 0;
padding-left: 1em;
}
.suite .suite {
margin: 5px;
}
.suite.passed {
background-color: #dfd;
}
.suite.failed {
background-color: #fdd;
}
.spec {
margin: 5px;
padding-left: 1em;
clear: both;
}
.spec.failed, .spec.passed, .spec.skipped {
padding-bottom: 5px;
border: 1px solid gray;
}
.spec.failed {
background-color: #fbb;
border-color: red;
}
.spec.passed {
background-color: #bfb;
border-color: green;
}
.spec.skipped {
background-color: #bbb;
}
.messages {
border-left: 1px dashed gray;
padding-left: 1em;
padding-right: 1em;
}
.passed {
background-color: #cfc;
display: none;
}
.failed {
background-color: #fbb;
}
.skipped {
color: #777;
background-color: #eee;
display: none;
}
/*.resultMessage {*/
/*white-space: pre;*/
/*}*/
.resultMessage span.result {
display: block;
line-height: 2em;
color: black;
}
.resultMessage .mismatch {
color: black;
}
.stackTrace {
white-space: pre;
font-size: .8em;
margin-left: 10px;
max-height: 5em;
overflow: auto;
border: 1px inset red;
padding: 1em;
background: #eef;
}
.finished-at {
padding-left: 1em;
font-size: .6em;
}
.show-passed .passed,
.show-skipped .skipped {
display: block;
}
#jasmine_content {
position:fixed;
right: 100%;
}
.runner {
border: 1px solid gray;
display: block;
margin: 5px 0;
padding: 2px 0 2px 10px;
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2009 Chris Wanstrath (Ruby)
Copyright (c) 2010 Jan Lehnardt (JavaScript)
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.
+344
View File
@@ -0,0 +1,344 @@
/*
* CommonJS-compatible mustache.js module
*
* See http://github.com/janl/mustache.js for more info.
*/
/*
mustache.js Ñ Logic-less templates in JavaScript
See http://mustache.github.com/ for more info.
*/
var Mustache = function() {
var Renderer = function() {};
Renderer.prototype = {
otag: "{{",
ctag: "}}",
pragmas: {},
buffer: [],
pragmas_implemented: {
"IMPLICIT-ITERATOR": true
},
context: {},
render: function(template, context, partials, in_recursion) {
// reset buffer & set context
if(!in_recursion) {
this.context = context;
this.buffer = []; // TODO: make this non-lazy
}
// fail fast
if(!this.includes("", template)) {
if(in_recursion) {
return template;
} else {
this.send(template);
return;
}
}
template = this.render_pragmas(template);
var html = this.render_section(template, context, partials);
if(in_recursion) {
return this.render_tags(html, context, partials, in_recursion);
}
this.render_tags(html, context, partials, in_recursion);
},
/*
Sends parsed lines
*/
send: function(line) {
if(line != "") {
this.buffer.push(line);
}
},
/*
Looks for %PRAGMAS
*/
render_pragmas: function(template) {
// no pragmas
if(!this.includes("%", template)) {
return template;
}
var that = this;
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
this.ctag);
return template.replace(regex, function(match, pragma, options) {
if(!that.pragmas_implemented[pragma]) {
throw({message:
"This implementation of mustache doesn't understand the '" +
pragma + "' pragma"});
}
that.pragmas[pragma] = {};
if(options) {
var opts = options.split("=");
that.pragmas[pragma][opts[0]] = opts[1];
}
return "";
// ignore unknown pragmas silently
});
},
/*
Tries to find a partial in the curent scope and render it
*/
render_partial: function(name, context, partials) {
name = this.trim(name);
if(!partials || partials[name] === undefined) {
throw({message: "unknown_partial '" + name + "'"});
}
if(typeof(context[name]) != "object") {
return this.render(partials[name], context, partials, true);
}
return this.render(partials[name], context[name], partials, true);
},
/*
Renders inverted (^) and normal (#) sections
*/
render_section: function(template, context, partials) {
if(!this.includes("#", template) && !this.includes("^", template)) {
return template;
}
var that = this;
// CSW - Added "+?" so it finds the tighest bound, not the widest
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
"\\s*", "mg");
// for each {{#foo}}{{/foo}} section do...
return template.replace(regex, function(match, type, name, content) {
var value = that.find(name, context);
if(type == "^") { // inverted section
if(!value || that.is_array(value) && value.length === 0) {
// false or empty list, render it
return that.render(content, context, partials, true);
} else {
return "";
}
} else if(type == "#") { // normal section
if(that.is_array(value)) { // Enumerable, Let's loop!
return that.map(value, function(row) {
return that.render(content, that.create_context(row),
partials, true);
}).join("");
} else if(that.is_object(value)) { // Object, Use it as subcontext!
return that.render(content, that.create_context(value),
partials, true);
} else if(typeof value === "function") {
// higher order section
return value.call(context, content, function(text) {
return that.render(text, context, partials, true);
});
} else if(value) { // boolean section
return that.render(content, context, partials, true);
} else {
return "";
}
}
});
},
/*
Replace {{foo}} and friends with values from our view
*/
render_tags: function(template, context, partials, in_recursion) {
// tit for tat
var that = this;
var new_regex = function() {
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
that.ctag + "+", "g");
};
var regex = new_regex();
var tag_replace_callback = function(match, operator, name) {
switch(operator) {
case "!": // ignore comments
return "";
case "=": // set new delimiters, rebuild the replace regexp
that.set_delimiters(name);
regex = new_regex();
return "";
case ">": // render partial
return that.render_partial(name, context, partials);
case "{": // the triple mustache is unescaped
return that.find(name, context);
default: // escape the value
return that.escape(that.find(name, context));
}
};
var lines = template.split("\n");
for(var i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
if(!in_recursion) {
this.send(lines[i]);
}
}
if(in_recursion) {
return lines.join("\n");
}
},
set_delimiters: function(delimiters) {
var dels = delimiters.split(" ");
this.otag = this.escape_regex(dels[0]);
this.ctag = this.escape_regex(dels[1]);
},
escape_regex: function(text) {
// thank you Simon Willison
if(!arguments.callee.sRE) {
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\'
];
arguments.callee.sRE = new RegExp(
'(\\' + specials.join('|\\') + ')', 'g'
);
}
return text.replace(arguments.callee.sRE, '\\$1');
},
/*
find `name` in current `context`. That is find me a value
from the view object
*/
find: function(name, context) {
name = this.trim(name);
// Checks whether a value is thruthy or false or 0
function is_kinda_truthy(bool) {
return bool === false || bool === 0 || bool;
}
var value = context;
var path = name.split(/\./);
for(var i = 0; i < path.length; i++) {
name = path[i];
if(value && is_kinda_truthy(value[name])) {
value = value[name];
} else if(i == 0 && is_kinda_truthy(this.context[name])) {
value = this.context[name];
} else {
value = undefined;
}
}
if(typeof value === "function") {
return value.apply(context);
}
if(value !== undefined) {
return value;
}
// silently ignore unkown variables
return "";
},
// Utility methods
/* includes tag */
includes: function(needle, haystack) {
return haystack.indexOf(this.otag + needle) != -1;
},
/*
Does away with nasty characters
*/
escape: function(s) {
s = String(s === null ? "" : s);
return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
switch(s) {
case "&": return "&amp;";
case "\\": return "\\\\";
case '"': return '&quot;';
case "'": return '&#39;';
case "<": return "&lt;";
case ">": return "&gt;";
default: return s;
}
});
},
// by @langalex, support for arrays of strings
create_context: function(_context) {
if(this.is_object(_context)) {
return _context;
} else {
var iterator = ".";
if(this.pragmas["IMPLICIT-ITERATOR"]) {
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
}
var ctx = {};
ctx[iterator] = _context;
return ctx;
}
},
is_object: function(a) {
return a && typeof a == "object";
},
is_array: function(a) {
return Object.prototype.toString.call(a) === '[object Array]';
},
/*
Gets rid of leading and trailing whitespace
*/
trim: function(s) {
return s.replace(/^\s*|\s*$/g, "");
},
/*
Why, why, why? Because IE. Cry, cry cry.
*/
map: function(array, fn) {
if (typeof array.map == "function") {
return array.map(fn);
} else {
var r = [];
var l = array.length;
for(var i = 0; i < l; i++) {
r.push(fn(array[i]));
}
return r;
}
}
};
return({
name: "mustache.js",
version: "0.3.1-dev",
/*
Turns a template and view into HTML
*/
to_html: function(template, view, partials, send_fun) {
var renderer = new Renderer();
if(send_fun) {
renderer.send = send_fun;
}
renderer.render(template, view, partials);
if(!send_fun) {
return renderer.buffer.join("\n");
}
}
});
}();
exports.name = Mustache.name;
exports.version = Mustache.version;
exports.to_html = function() {
return Mustache.to_html.apply(this, arguments);
};
+236 -17
View File
@@ -1,22 +1,241 @@
var sys = require('sys'),
http = require('http'),
fs = require('fs');
fs = require('fs'),
url = require('url'),
events = require('events');
http.createServer(function (req, res) {
res.writeHead(200, {});
sys.p('GET ' + req.url);
var file = fs.createReadStream('.' + req.url);
file.addListener('data', bind(res, res.write));
file.addListener('error', function( error ){
sys.p(error);
res.end();
});
file.addListener('close', bind(res, res.end));
}).listen(8000);
sys.puts('Server running at http://127.0.0.1:8000/');
var DEFAULT_PORT = 8000;
function bind(_this, _fn) {
return function(){
return _fn.apply(_this, arguments);
};
function main(argv) {
new HttpServer({
'GET': createServlet(StaticServlet),
'HEAD': createServlet(StaticServlet)
}).start(Number(argv[2]) || DEFAULT_PORT);
}
function escapeHtml(value) {
return value.toString().
replace('<', '&lt;').
replace('>', '&gt').
replace('"', '&quot;');
}
function createServlet(Class) {
var servlet = new Class();
return servlet.handleRequest.bind(servlet);
}
/**
* An Http server implementation that uses a map of methods to decide
* action routing.
*
* @param {Object} Map of method => Handler function
*/
function HttpServer(handlers) {
this.handlers = handlers;
this.server = http.createServer(this.handleRequest_.bind(this));
}
HttpServer.prototype.start = function(port) {
this.port = port;
this.server.listen(port);
sys.puts('Http Server running at http://127.0.0.1:' + port + '/');
};
HttpServer.prototype.parseUrl_ = function(urlString) {
var parsed = url.parse(urlString);
parsed.pathname = url.resolve('/', parsed.pathname);
return url.parse(url.format(parsed), true);
};
HttpServer.prototype.handleRequest_ = function(req, res) {
var logEntry = req.method + ' ' + req.url;
if (req.headers['user-agent']) {
logEntry += ' ' + req.headers['user-agent'];
}
sys.puts(logEntry);
req.url = this.parseUrl_(req.url);
var handler = this.handlers[req.method];
if (!handler) {
res.writeHead(501);
res.end();
} else {
handler.call(this, req, res);
}
};
/**
* Handles static content.
*/
function StaticServlet() {}
StaticServlet.MimeMap = {
'txt': 'text/plain',
'html': 'text/html',
'css': 'text/css',
'xml': 'application/xml',
'json': 'application/json',
'js': 'application/javascript',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'png': 'image/png'
};
StaticServlet.prototype.handleRequest = function(req, res) {
var self = this;
var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/, function(match, hex){
return String.fromCharCode(parseInt(hex, 16));
});
var parts = path.split('/');
if (parts[parts.length-1].charAt(0) === '.')
return self.sendForbidden_(req, res, path);
fs.stat(path, function(err, stat) {
if (err)
return self.sendMissing_(req, res, path);
if (stat.isDirectory())
return self.sendDirectory_(req, res, path);
return self.sendFile_(req, res, path);
});
}
StaticServlet.prototype.sendError_ = function(req, res, error) {
res.writeHead(500, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>Internal Server Error</title>\n');
res.write('<h1>Internal Server Error</h1>');
res.write('<pre>' + escapeHtml(sys.inspect(error)) + '</pre>');
sys.puts('500 Internal Server Error');
sys.puts(sys.inspect(error));
};
StaticServlet.prototype.sendMissing_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>404 Not Found</title>\n');
res.write('<h1>Not Found</h1>');
res.write(
'<p>The requested URL ' +
escapeHtml(path) +
' was not found on this server.</p>'
);
res.end();
sys.puts('404 Not Found: ' + path);
};
StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(403, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>403 Forbidden</title>\n');
res.write('<h1>Forbidden</h1>');
res.write(
'<p>You do not have permission to access ' +
escapeHtml(path) + ' on this server.</p>'
);
res.end();
sys.puts('403 Forbidden: ' + path);
};
StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
res.writeHead(301, {
'Content-Type': 'text/html',
'Location': redirectUrl
});
res.write('<!doctype html>\n');
res.write('<title>301 Moved Permanently</title>\n');
res.write('<h1>Moved Permanently</h1>');
res.write(
'<p>The document has moved <a href="' +
redirectUrl +
'">here</a>.</p>'
);
res.end();
sys.puts('401 Moved Permanently: ' + redirectUrl);
};
StaticServlet.prototype.sendFile_ = function(req, res, path) {
var self = this;
var file = fs.createReadStream(path);
res.writeHead(200, {
'Content-Type': StaticServlet.
MimeMap[path.split('.').pop()] || 'text/plain'
});
if (req.method === 'HEAD') {
res.end();
} else {
file.on('data', res.write.bind(res));
file.on('close', function() {
res.end();
});
file.on('error', function(error) {
self.sendError_(req, res, error);
});
}
};
StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
var self = this;
if (path.match(/[^\/]$/)) {
req.url.pathname += '/';
var redirectUrl = url.format(url.parse(url.format(req.url)));
return self.sendRedirect_(req, res, redirectUrl);
}
fs.readdir(path, function(err, files) {
if (err)
return self.sendError_(req, res, error);
if (!files.length)
return self.writeDirectoryIndex_(req, res, path, []);
var remaining = files.length;
files.forEach(function(fileName, index) {
fs.stat(path + '/' + fileName, function(err, stat) {
if (err)
return self.sendError_(req, res, err);
if (stat.isDirectory()) {
files[index] = fileName + '/';
}
if (!(--remaining))
return self.writeDirectoryIndex_(req, res, path, files);
});
});
});
};
StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
path = path.substring(1);
res.writeHead(200, {
'Content-Type': 'text/html'
});
if (req.method === 'HEAD') {
res.end();
return;
}
res.write('<!doctype html>\n');
res.write('<title>' + escapeHtml(path) + '</title>\n');
res.write('<style>\n');
res.write(' ol { list-style-type: none; font-size: 1.2em; }\n');
res.write('</style>\n');
res.write('<h1>Directory: ' + escapeHtml(path) + '</h1>');
res.write('<ol>');
files.forEach(function(fileName) {
if (fileName.charAt(0) !== '.') {
res.write('<li><a href="' +
escapeHtml(fileName) + '">' +
escapeHtml(fileName) + '</a></li>');
}
});
res.write('</ol>');
res.end();
};
// Must be last,
main(process.argv);
+6
View File
@@ -0,0 +1,6 @@
var fs = require('fs');
var filename = __dirname + '/showdown-0.9.js';
var src = fs.readFileSync(filename);
var Showdown = process.compile(src + '\nShowdown;', filename);
exports.Showdown = Showdown;
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+1 -1
View File
@@ -1 +1 @@
node lib/nodeserver/server.js
node lib/nodeserver/server.js $1
-1
View File
@@ -2,7 +2,6 @@
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</script>
</head>
<body ng:init="$window.$root = this; data = [{foo: 'foo'},{bar: 'bar'}]">
<p>This is a demo of a potential bug in angular.</p>
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</head>
<body ng:init="$window.$root = this; data = [{foo: 'foo'},{bar: 'bar'}]">
<ng:include src="'ng_include_this.partial'" scope="this"/>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
included HTML. eval count: {{c=c+1}}
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="../angular-scenario.js"></script>
<script type="text/javascript" src="../build/angular-scenario.js"></script>
<script type="text/javascript" src="widgets-scenario.js"></script>
</head>
<body>
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="../src/scenario/bootstrap.js"></script>
<script type="text/javascript" src="../src/scenario/angular-bootstrap.js"></script>
<script type="text/javascript" src="widgets-scenario.js"></script>
</head>
<body>
+19 -5
View File
@@ -1,6 +1,6 @@
describe('widgets', function() {
it('should verify that basic widgets work', function(){
navigateTo('widgets.html');
browser().navigateTo('widgets.html');
using('#text-basic-box').input('text.basic').enter('Carlos');
expect(binding('text.basic')).toEqual('Carlos');
@@ -27,15 +27,29 @@ describe('widgets', function() {
expect(binding('multiselect').fromJson()).toEqual(['A', 'C']);
expect(binding('button').fromJson()).toEqual({'count': 0});
element('form a').click();
expect(binding('form').fromJson()).toEqual({'count': 0});
element('form a', "'action' link").click();
expect(binding('button').fromJson()).toEqual({'count': 1});
element('input[value="submit"]').click();
element('input[value="submit input"]', "'submit input' button").click();
expect(binding('button').fromJson()).toEqual({'count': 2});
element('input[value="button"]').click();
expect(binding('form').fromJson()).toEqual({'count': 1});
element('button:contains("submit button")', "'submit button' button").click();
expect(binding('button').fromJson()).toEqual({'count': 2});
expect(binding('form').fromJson()).toEqual({'count': 2});
element('input[value="button"]', "'button' button").click();
expect(binding('button').fromJson()).toEqual({'count': 3});
element('input[type="image"]').click();
element('input[type="image"]', 'form image').click();
expect(binding('button').fromJson()).toEqual({'count': 4});
element('#navigate a', "'Go to #route' link").click();
expect(binding('$location.hash')).toEqual('route');
expect(browser().location().hash()).toEqual('route');
/**
* Custom value parser for futures.
*/
+10 -5
View File
@@ -2,7 +2,6 @@
<html xmlns:ng="http://angularjs.org">
<head>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="../libs/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</head>
<body ng:init="$window.$scope = this">
@@ -74,15 +73,16 @@
<tr><th colspan="3">Buttons</th></tr>
<tr>
<td>ng:change<br/>ng:click</td>
<td ng:init="button.count = 0">
<form>
<td ng:init="button.count = 0; form.count = 0;">
<form ng:submit="form.count = form.count + 1">
<input type="button" value="button" ng:change="button.count = button.count + 1"/> <br/>
<input type="submit" value="submit" ng:change="button.count = button.count + 1"/><br/>
<input type="submit" value="submit input" ng:change="button.count = button.count + 1"/><br/>
<button type="submit">submit button</button>
<input type="image" src="" ng:change="button.count = button.count + 1"/><br/>
<a href="" ng:click="button.count = button.count + 1">action</a>
</form>
</td>
<td>button={{button}}</td>
<td>button={{button}} form={{form}}</td>
</tr>
<tr><th colspan="3">Repeaters</th></tr>
<tr id="repeater-row">
@@ -94,6 +94,11 @@
</td>
<td></td>
</tr>
<tr id="navigate">
<td>navigate</td>
<td><a href="#route">Go to #route</td>
<td>{{$location.hash}}</td>
</tr>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 20000 --config jsTestDriver-coverage.conf
+1 -1
View File
@@ -1 +1 @@
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 20000
+658 -23
View File
@@ -3,6 +3,53 @@
if (typeof document.getAttribute == $undefined)
document.getAttribute = function() {};
/**
* @workInProgress
* @ngdoc function
* @name angular.lowercase
* @function
*
* @description Converts string to lowercase
* @param {string} string String to be lowercased.
* @returns {string} Lowercased string.
*/
var lowercase = function (string){ return isString(string) ? string.toLowerCase() : string; };
/**
* @workInProgress
* @ngdoc function
* @name angular.uppercase
* @function
*
* @description Converts string to uppercase.
* @param {string} string String to be uppercased.
* @returns {string} Uppercased string.
*/
var uppercase = function (string){ return isString(string) ? string.toUpperCase() : string; };
var manualLowercase = function (s) {
return isString(s) ? s.replace(/[A-Z]/g,
function (ch) {return fromCharCode(ch.charCodeAt(0) | 32); }) : s;
};
var manualUppercase = function (s) {
return isString(s) ? s.replace(/[a-z]/g,
function (ch) {return fromCharCode(ch.charCodeAt(0) & ~32); }) : s;
};
// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods with
// correct but slower alternatives.
if ('i' !== 'I'.toLowerCase()) {
lowercase = manualLowercase;
uppercase = manualUppercase;
}
function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$element = '$element',
@@ -30,6 +77,7 @@ var _undefined = undefined,
PRIORITY_WATCH = -1000,
PRIORITY_LAST = 99999,
PRIORITY = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH},
Error = window.Error,
jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy
_ = window['_'],
/** holds major version number for IE or NaN for real browsers */
@@ -38,14 +86,435 @@ var _undefined = undefined,
slice = Array.prototype.slice,
push = Array.prototype.push,
error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop,
/**
* @workInProgress
* @ngdoc overview
* @name angular
* @namespace The exported angular namespace.
*/
angular = window[$angular] || (window[$angular] = {}),
angularTextMarkup = extensionMap(angular, 'markup'),
angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
/**
* @workInProgress
* @ngdoc overview
* @name angular.directive
* @namespace Namespace for all directives.
*
* @description
* A directive is an HTML attribute that you can use in an existing HTML element type or in a
* DOM element type that you create as {@link angular.widget}, to modify that element's
* properties. You can use any number of directives per element.
*
* For example, you can add the ng:bind directive as an attribute of an HTML span element, as in
* `<span ng:bind="1+2"></span>`. How does this work? The compiler passes the attribute value
* `1+2` to the ng:bind extension, which in turn tells the {@link angular.scope} to watch that
* expression and report changes. On any change it sets the span text to the expression value.
*
* Here's how to define {@link angular.directive.ng:bind ng:bind}:
* <pre>
angular.directive('ng:bind', function(expression, compiledElement) {
var compiler = this;
return function(linkElement) {
var currentScope = this;
currentScope.$watch(expression, function(value) {
linkElement.text(value);
});
};
});
* </pre>
*
* # Directive vs. Attribute Widget
* Both [attribute widgets](#!angular.widget) and directives can compile a DOM element
* attribute. So why have two different ways to do the same thing? The answer is that order
* matters, but we have no control over the order in which attributes are read. To solve this
* we apply attribute widget before the directive.
*
* For example, consider this piece of HTML, which uses the directives `ng:repeat`, `ng:init`,
* and `ng:bind`:
* <pre>
<ul ng:init="people=['mike', 'mary']">
<li ng:repeat="person in people" ng:init="a=a+1" ng:bind="person"></li>
</ul>
* </pre>
*
* Notice that the order of execution matters here. We need to execute
* {@link angular.directive.ng:repeat ng:repeat} before we run the
* {@link angular.directive.ng:init ng:init} and `ng:bind` on the `<li/>;`. This is because we
* want to run the `ng:init="a=a+1` and `ng:bind="person"` once for each person in people. We
* could not have used directive to create this template because attributes are read in an
* unspecified order and there is no way of guaranteeing that the repeater attribute would
* execute first. Using the `ng:repeat` attribute directive ensures that we can transform the
* DOM element into a template.
*
* Widgets run before directives. Widgets may manipulate the DOM whereas directives are not
* expected to do so, and so they run last.
*/
angularDirective = extensionMap(angular, 'directive'),
/**
* @workInProgress
* @ngdoc overview
* @name angular.widget
* @namespace Namespace for all widgets.
* @description
* # Overview
* Widgets allow you to create DOM elements that the browser doesn't
* already understand. You create the widget in your namespace and
* assign it behavior. You can only bind one widget per DOM element
* (unlike directives, in which you can use any number per DOM
* element). Widgets are expected to manipulate the DOM tree by
* adding new elements whereas directives are expected to only modify
* element properties.
*
* Widgets come in two flavors: element and attribute.
*
* # Element Widget
* Let's say we would like to create a new element type in the
* namespace `my` that can watch an expression and alert() the user
* with each new value.
*
* <pre>
* &lt;my:watch exp="name"/&gt;
* </pre>
*
* You can implement `my:watch` like this:
* <pre>
* angular.widget('my:watch', function(compileElement) {
* var compiler = this;
* var exp = compileElement.attr('exp');
* return function(linkElement) {
* var currentScope = this;
* currentScope.$watch(exp, function(value){
* alert(value);
* }};
* };
* });
* </pre>
*
* # Attribute Widget
* Let's implement the same widget, but this time as an attribute
* that can be added to any existing DOM element.
* <pre>
* &lt;div my-watch="name"&gt;text&lt;/div&gt;
* </pre>
* You can implement `my:watch` attribute like this:
* <pre>
* angular.widget('@my:watch', function(expression, compileElement) {
* var compiler = this;
* return function(linkElement) {
* var currentScope = this;
* currentScope.$watch(expression, function(value){
* alert(value);
* });
* };
* });
* </pre>
*
* @example
* <script>
* angular.widget('my:time', function(compileElement){
* compileElement.css('display', 'block');
* return function(linkElement){
* function update(){
* linkElement.text('Current time is: ' + new Date());
* setTimeout(update, 1000);
* }
* update();
* };
* });
* </script>
* <my:time></my:time>
*/
angularWidget = extensionMap(angular, 'widget', lowercase),
/**
* @workInProgress
* @ngdoc overview
* @name angular.validator
* @namespace Namespace for all filters.
* @description
* # Overview
* Validators are a standard way to check the user input against a specific criteria. For
* example, you might need to check that an input field contains a well-formed phone number.
*
* # Syntax
* Attach a validator on user input widgets using the `ng:validate` attribute.
*
* <doc:example>
* <doc:source>
* Change me: &lt;input type="text" name="number" ng:validate="integer" value="123"&gt;
* </doc:source>
* <doc:scenario>
* it('should validate the default number string', function() {
* expect(element('input[name=number]').attr('class')).
* not().toMatch(/ng-validation-error/);
* });
* it('should not validate "foo"', function() {
* input('number').enter('foo');
* expect(element('input[name=number]').attr('class')).
* toMatch(/ng-validation-error/);
* });
* </doc:scenario>
* </doc:example>
*
*
* # Writing your own Validators
* Writing your own validator is easy. To make a function available as a
* validator, just define the JavaScript function on the `angular.validator`
* object. <angular/> passes in the input to validate as the first argument
* to your function. Any additional validator arguments are passed in as
* additional arguments to your function.
*
* You can use these variables in the function:
*
* * `this` — The current scope.
* * `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
* the DOM in addition to transforming the input.
*
* In this example we have written a upsTrackingNo validator.
* It marks the input text "valid" only when the user enters a well-formed
* UPS tracking number.
*
* @css ng-validation-error
* When validation fails, this css class is applied to the binding, making its borders red by
* default.
*
* @example
* <script>
* angular.validator('upsTrackingNo', function(input, format) {
* var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
* return input.match(regexp)?"":"The format must match " + format;
* });
* </script>
* <input type="text" name="trackNo" size="40"
* ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
* value="1Z 123 456 78 9012 345 6"/>
*
* @scenario
* it('should validate correct UPS tracking number', function() {
* expect(element('input[name=trackNo]').attr('class')).
* not().toMatch(/ng-validation-error/);
* });
*
* it('should not validate in correct UPS tracking number', function() {
* input('trackNo').enter('foo');
* expect(element('input[name=trackNo]').attr('class')).
* toMatch(/ng-validation-error/);
* });
*
*/
angularValidator = extensionMap(angular, 'validator'),
/**
* @workInProgress
* @ngdoc overview
* @name angular.filter
* @namespace Namespace for all filters.
* @description
* # Overview
* Filters are a standard way to format your data for display to the user. For example, you
* might have the number 1234.5678 and would like to display it as US currency: $1,234.57.
* Filters allow you to do just that. In addition to transforming the data, filters also modify
* the DOM. This allows the filters to for example apply css styles to the filtered output if
* certain conditions were met.
*
*
* # Standard Filters
*
* The Angular framework provides a standard set of filters for common operations, including:
* {@link angular.filter.currency currency}, {@link angular.filter.json json},
* {@link angular.filter.number number}, and {@link angular.filter.html html}. You can also add
* your own filters.
*
*
* # Syntax
*
* Filters can be part of any {@link angular.scope} evaluation but are typically used with
* {{bindings}}. Filters typically transform the data to a new data type, formating the data in
* the process. Filters can be chained and take optional arguments. Here are few examples:
*
* * No filter: {{1234.5678}} => 1234.5678
* * Number filter: {{1234.5678|number}} => 1,234.57. Notice the “,” and rounding to two
* significant digits.
* * Filter with arguments: {{1234.5678|number:5}} => 1,234.56780. Filters can take optional
* arguments, separated by colons in a binding. To number, the argument “5” requests 5 digits
* to the right of the decimal point.
*
*
* # Writing your own Filters
*
* Writing your own filter is very easy: just define a JavaScript function on `angular.filter`.
* The framework passes in the input value as the first argument to your function. Any filter
* arguments are passed in as additional function arguments.
*
* You can use these variables in the function:
*
* * `this` — The current scope.
* * `this.$element` — The DOM element containing the binding. This allows the filter to manipulate
* the DOM in addition to transforming the input.
*
*
* @exampleDescription
* The following example filter reverses a text string. In addition, it conditionally makes the
* text upper-case (to demonstrate optional arguments) and assigns color (to demonstrate DOM
* modification).
*
* @example
<script type="text/javascript">
angular.filter('reverse', function(input, uppercase, color) {
var out = "";
for (var i = 0; i < input.length; i++) {
out = input.charAt(i) + out;
}
if (uppercase) {
out = out.toUpperCase();
}
if (color) {
this.$element.css('color', color);
}
return out;
});
</script>
<input name="text" type="text" value="hello" /><br>
No filter: {{text}}<br>
Reverse: {{text|reverse}}<br>
Reverse + uppercase: {{text|reverse:true}}<br>
Reverse + uppercase + blue: {{text|reverse:true:"blue"}}
*/
angularFilter = extensionMap(angular, 'filter'),
/**
* @workInProgress
* @ngdoc overview
* @name angular.formatter
* @namespace Namespace for all formats.
* @description
* # Overview
* The formatters are responsible for translating user readable text in an input widget to a
* data model stored in an application.
*
* # Writting your own Formatter
* Writing your own formatter is easy. Just register a pair of JavaScript functions with
* `angular.formatter`. One function for parsing user input text to the stored form,
* and one for formatting the stored data to user-visible text.
*
* Here is an example of a "reverse" formatter: The data is stored in uppercase and in
* reverse, while it is displayed in lower case and non-reversed. User edits are
* automatically parsed into the internal form and data changes are automatically
* formatted to the viewed form.
*
* <pre>
* function reverse(text) {
* var reversed = [];
* for (var i = 0; i < text.length; i++) {
* reversed.unshift(text.charAt(i));
* }
* return reversed.join('');
* }
*
* angular.formatter('reverse', {
* parse: function(value){
* return reverse(value||'').toUpperCase();
* },
* format: function(value){
* return reverse(value||'').toLowerCase();
* }
* });
* </pre>
*
* @example
* <script type="text/javascript">
* function reverse(text) {
* var reversed = [];
* for (var i = 0; i < text.length; i++) {
* reversed.unshift(text.charAt(i));
* }
* return reversed.join('');
* }
*
* angular.formatter('reverse', {
* parse: function(value){
* return reverse(value||'').toUpperCase();
* },
* format: function(value){
* return reverse(value||'').toLowerCase();
* }
* });
* </script>
*
* Formatted:
* <input type="text" name="data" value="angular" ng:format="reverse"/>
* <br/>
*
* Stored:
* <input type="text" name="data"/><br/>
* <pre>{{data}}</pre>
*
*
* @scenario
* it('should store reverse', function(){
* expect(element('.doc-example input:first').val()).toEqual('angular');
* expect(element('.doc-example input:last').val()).toEqual('RALUGNA');
*
* this.addFutureAction('change to XYZ', function($window, $document, done){
* $document.elements('.doc-example input:last').val('XYZ').trigger('change');
* done();
* });
* expect(element('.doc-example input:first').val()).toEqual('zyx');
* });
*/
angularFormatter = extensionMap(angular, 'formatter'),
/**
* @workInProgress
* @ngdoc overview
* @name angular.service
*
* @description
* # Overview
* Services are substituable objects, which are wired together using dependency injection.
* Each service could have dependencies (other services), which are passed in constructor.
* Because JS is dynamicaly typed language, dependency injection can not use static types
* to satisfy these dependencies, so each service must explicitely define its dependencies.
* This is done by $inject property.
*
* For now, life time of all services is the same as the life time of page.
*
*
* # Standard services
* The Angular framework provides a standard set of services for common operations.
* You can write your own services and rewrite these standard services as well.
* Like other core angular variables, standard services always start with $.
*
* * `angular.service.$window`
* * `angular.service.$document`
* * `angular.service.$location`
* * `angular.service.$log`
* * `angular.service.$exceptionHandler`
* * `angular.service.$hover`
* * `angular.service.$invalidWidgets`
* * `angular.service.$route`
* * `angular.service.$xhr`
* * `angular.service.$xhr.error`
* * `angular.service.$xhr.bulk`
* * `angular.service.$xhr.cache`
* * `angular.service.$resource`
* * `angular.service.$cookies`
* * `angular.service.$cookieStore`
*
* # Writing your own services
* <pre>
* angular.service('notify', function(location) {
* this.one = function() {
* }
* }, {$inject: ['$location']});
* </pre>
*
* # Using services in controller
*/
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
nodeName,
@@ -84,6 +553,18 @@ function foreachSorted(obj, iterator, context) {
}
function formatError(arg) {
if (arg instanceof Error) {
if (arg.stack) {
arg = arg.stack;
} else if (arg.sourceURL) {
arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line;
}
}
return arg;
}
function extend(dst) {
foreach(arguments, function(obj){
if (obj !== dst) {
@@ -102,11 +583,18 @@ function inherit(parent, extra) {
function noop() {}
function identity($) {return $;}
function valueFn(value) {return function(){ return value; };}
function extensionMap(angular, name, transform) {
var extPoint;
return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
name = (transform || identity)(name);
if (isDefined(fn)) {
if (isDefined(extPoint[name])) {
foreach(extPoint[name], function(property, key) {
if (key.charAt(0) == '$' && isUndefined(fn[key]))
fn[key] = property;
});
}
extPoint[name] = extend(fn, prop || {});
}
return extPoint[name];
@@ -131,18 +619,31 @@ function isDefined(value){ return typeof value != $undefined; }
function isObject(value){ return value!=_null && typeof value == $object;}
function isString(value){ return typeof value == $string;}
function isNumber(value){ return typeof value == $number;}
function isDate(value){ return value instanceof Date; }
function isArray(value) { return value instanceof Array; }
function isFunction(value){ return typeof value == $function;}
function isBoolean(value) { return typeof value == $boolean;}
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) {
/**
* HTML class which is the only class which can be used in ng:bind to inline HTML for security reasons.
* @constructor
* @param html raw (unsafe) html
* @param {string=} option if set to 'usafe' then get method will return raw (unsafe/unsanitized) html
*/
function HTML(html, option) {
this.html = html;
this.get = lowercase(option) == 'unsafe' ?
valueFn(html) :
function htmlSanitize() {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf));
return buf.join('');
};
}
if (msie) {
@@ -225,7 +726,7 @@ function isLeafNode (node) {
* @param {*} source The source to be used during copy.
* Can be any type including primitives, null and undefined.
* @param {(Object|Array)=} destination Optional destination into which the source is copied
* @return {*}
* @returns {*}
*/
function copy(source, destination){
if (!destination) {
@@ -233,7 +734,7 @@ function copy(source, destination){
if (source) {
if (isArray(source)) {
destination = copy(source, []);
} else if (source instanceof Date) {
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isObject(source)) {
destination = copy(source, {});
@@ -297,16 +798,6 @@ function setHtml(node, html) {
}
}
function escapeHtml(html) {
if (!html || !html.replace)
return html;
return html.
replace(/&/g, '&amp;').
replace(/</g, '&lt;').
replace(/>/g, '&gt;');
}
function isRenderableElement(element) {
var name = element && element[0] && element[0].nodeName;
return name && name.charAt(0) != '#' &&
@@ -328,13 +819,6 @@ function elementError(element, type, error) {
}
}
function escapeAttr(html) {
if (!html || !html.replace)
return html;
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g,
'&quot;');
}
function concat(array1, array2, index) {
return array1.concat(slice.call(array2, index, array2.length));
}
@@ -385,7 +869,7 @@ function compile(element, existingScope) {
/**
* Parses an escaped url query string into key-value pairs.
* @return Object.<(string|boolean)>
* @returns Object.<(string|boolean)>
*/
function parseKeyValue(/**string*/keyValue) {
var obj = {}, key_value, key;
@@ -407,6 +891,157 @@ function toKeyValue(obj) {
return parts.length ? parts.join('&') : '';
}
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:autobind
* @element script
*
* @TODO ng:autobind is not a directive!! it should be documented as bootstrap parameter in a
* separate bootstrap section.
* @TODO rename to ng:autobind to ng:autoboot
*
* @description
* This section explains how to bootstrap your application with angular using either the angular
* javascript file.
*
*
* ## The angular distribution
* Note that there are two versions of the angular javascript file that you can use:
*
* * `angular.js` - the development version - this file is unobfuscated, uncompressed, and thus
* human-readable and useful when developing your angular applications.
* * `angular.min.js` - the production version - this is a minified and obfuscated version of
* `angular.js`. You want to use this version when you want to load a smaller but functionally
* equivalent version of the code in your application. We use the Closure compiler to create this
* file.
*
*
* ## Auto-bootstrap with `ng:autobind`
* The simplest way to get an <angular/> application up and running is by inserting a script tag in
* your HTML file that bootstraps the `http://code.angularjs.org/angular-x.x.x.min.js` code and uses
* the special `ng:autobind` attribute, like in this snippet of HTML:
*
* <pre>
&lt;!doctype html&gt;
&lt;html xmlns:ng="http://angularjs.org"&gt;
&lt;head&gt;
&lt;script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
ng:autobind&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
Hello {{'world'}}!
&lt;/body&gt;
&lt;/html&gt;
* </pre>
*
* The `ng:autobind` attribute tells <angular/> to compile and manage the whole HTML document. The
* compilation occurs in the page's `onLoad` handler. Note that you don't need to explicitly add an
* `onLoad` event; auto bind mode takes care of all the magic for you.
*
*
* ## Auto-bootstrap with `#autobind`
* In rare cases when you can't define the `ng` namespace before the script tag (e.g. in some CMS
* systems, etc), it is possible to auto-bootstrap angular by appending `#autobind` to the script
* src URL, like in this snippet:
*
* <pre>
&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;script type="text/javascript"
src="http://code.angularjs.org/angular-0.9.3.min.js#autobind"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div xmlns:ng="http://angularjs.org"&gt;
Hello {{'world'}}!
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
* </pre>
*
* In this case it's the `#autobind` URL fragment that tells angular to auto-bootstrap.
*
*
* ## Filename Restrictions for Auto-bootstrap
* In order for us to find the auto-bootstrap script attribute or URL fragment, the value of the
* `script` `src` attribute that loads angular script must match one of these naming
* conventions:
*
* - `angular.js`
* - `angular-min.js`
* - `angular-x.x.x.js`
* - `angular-x.x.x.min.js`
* - `angular-x.x.x-xxxxxxxx.js` (dev snapshot)
* - `angular-x.x.x-xxxxxxxx.min.js` (dev snapshot)
* - `angular-bootstrap.js` (used for development of angular)
*
* Optionally, any of the filename format above can be prepended with relative or absolute URL that
* ends with `/`.
*
*
* ## Manual Bootstrap
* Using auto-bootstrap is a handy way to start using <angular/>, but advanced users who want more
* control over the initialization process might prefer to use manual bootstrap instead.
*
* The best way to get started with manual bootstraping is to look at the magic behind `ng:autobind`
* by writing out each step of the autobind process explicitly. Note that the following code is
* equivalent to the code in the previous section.
*
* <pre>
&lt;!doctype html&gt;
&lt;html xmlns:ng="http://angularjs.org"&gt;
&lt;head&gt;
&lt;script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
ng:autobind&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
(function(window, previousOnLoad){
window.onload = function(){
try { (previousOnLoad||angular.noop)(); } catch(e) {}
angular.compile(window.document).$init();
};
})(window, window.onload);
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
Hello {{'World'}}!
&lt;/body&gt;
&lt;/html&gt;
* </pre>
*
* This is the sequence that your code should follow if you're bootstrapping angular on your own:
*
* * After the page is loaded, find the root of the HTML template, which is typically the root of
* the document.
* * Run the HTML compiler, which converts the templates into an executable, bi-directionally bound
* application.
*
*
* ##XML Namespace
* *IMPORTANT:* When using <angular/> you must declare the ng namespace using the xmlns tag. If you
* don't declare the namespace, Internet Explorer does not render widgets properly.
*
* <pre>
* &lt;html xmlns:ng="http://angularjs.org"&gt;
* </pre>
*
*
* ## Create your own namespace
* If you want to define your own widgets, you must create your own namespace and use that namespace
* to form the fully qualified widget name. For example, you could map the alias `my` to your domain
* and create a widget called my:widget. To create your own namespace, simply add another xmlsn tag
* to your page, create an alias, and set it to your unique domain:
*
* <pre>
* &lt;html xmlns:ng="http://angularjs.org" xmlns:my="http://mydomain.com"&gt;
* </pre>
*
*
* ## Global Object
* The <angular/> script creates a single global variable `angular` in the global namespace. All
* APIs are bound to fields of this global object.
*
*/
function angularInit(config){
if (config.autobind) {
// TODO default to the source of angular.js
+9
View File
@@ -1,4 +1,13 @@
var browserSingleton;
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$browser
* @requires $log
*
* @description
* Represents the browser.
*/
angularService('$browser', function($log){
if (!browserSingleton) {
browserSingleton = new Browser(
+145 -27
View File
@@ -19,7 +19,21 @@ function Browser(location, document, head, XHR, $log) {
var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];
self.xhr = function(method, url, post, callback){
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#xhr
* @methodOf angular.service.$browser
*
* @param {string} method Requested method (get|post|put|delete|head|json)
* @param {string} url Requested url
* @param {string=} post Post data to send
* @param {function(number, string)} callback Function that will be called on response
*
* @description
* Send ajax request
*/
self.xhr = function(method, url, post, callback) {
if (isFunction(post)) {
callback = post;
post = _null;
@@ -63,7 +77,15 @@ function Browser(location, document, head, XHR, $log) {
}
};
self.notifyWhenNoOutstandingRequests = function(callback){
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#notifyWhenNoOutstandingRequests
* @methodOf angular.service.$browser
*
* @param {function()} callback Function that will be called when no outstanding request
*/
self.notifyWhenNoOutstandingRequests = function(callback) {
if (outstandingRequestCount === 0) {
callback();
} else {
@@ -75,27 +97,51 @@ function Browser(location, document, head, XHR, $log) {
// Poll Watcher API
//////////////////////////////////////////////////////////////
var pollFns = [];
function poll(){
foreach(pollFns, function(pollFn){ pollFn(); });
}
self.poll = poll;
/**
* Adds a function to the list of functions that poller periodically executes
* @return {Function} the added function
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#poll
* @methodOf angular.service.$browser
*/
self.addPollFn = function(/**Function*/fn){
self.poll = function() {
foreach(pollFns, function(pollFn){ pollFn(); });
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addPollFn
* @methodOf angular.service.$browser
*
* @param {function()} fn Poll function to add
*
* @description
* Adds a function to the list of functions that poller periodically executes
*
* @returns {function()} the added function
*/
self.addPollFn = function(fn) {
pollFns.push(fn);
return fn;
};
/**
* Configures the poller to run in the specified intervals, using the specified setTimeout fn and
* kicks it off.
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#startPoller
* @methodOf angular.service.$browser
*
* @param {number} interval How often should browser call poll functions (ms)
* @param {function()} setTimeout Reference to a real or fake `setTimeout` function.
*
* @description
* Configures the poller to run in the specified intervals, using the specified
* setTimeout fn and kicks it off.
*/
self.startPoller = function(/**number*/interval, /**Function*/setTimeout){
self.startPoller = function(interval, setTimeout) {
(function check(){
poll();
self.poll();
setTimeout(check, interval);
})();
};
@@ -103,15 +149,39 @@ function Browser(location, document, head, XHR, $log) {
//////////////////////////////////////////////////////////////
// URL API
//////////////////////////////////////////////////////////////
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#setUrl
* @methodOf angular.service.$browser
*
* @param {string} url New url
*
* @description
* Sets browser's url
*/
self.setUrl = function(url) {
var existingURL = location.href;
if (!existingURL.match(/#/)) existingURL += '#';
if (!url.match(/#/)) url += '#';
location.href = url;
};
self.getUrl = function() {
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#getUrl
* @methodOf angular.service.$browser
*
* @description
* Get current browser's url
*
* @returns {string} Browser's url
*/
self.getUrl = function() {
return location.href;
};
};
//////////////////////////////////////////////////////////////
// Cookies API
@@ -121,19 +191,28 @@ function Browser(location, document, head, XHR, $log) {
var lastCookieString = '';
/**
* The cookies method provides a 'private' low level access to browser cookies. It is not meant to
* be used directly, use the $cookie service instead.
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#cookies
* @methodOf angular.service.$browser
*
* @param {string=} name Cookie name
* @param {string=} value Cokkie value
*
* @description
* The cookies method provides a 'private' low level access to browser cookies.
* It is not meant to be used directly, use the $cookie service instead.
*
* The return values vary depending on the arguments that the method was called with as follows:
* <ul><li>
* cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
* </li><li>
* cookies(name, value) -> set name to value, if value is undefined delete the cookie
* </li><li>
* cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
* </li></ul>
* <ul>
* <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li>
* <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li>
* <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li>
* </ul>
*
* @returns {Object} Hash of all cookies (if called without any parameter)
*/
self.cookies = function (/**string*/name, /**string*/value){
self.cookies = function (name, value) {
var cookieLength, cookieArray, i, keyValue;
if (name) {
@@ -175,7 +254,30 @@ function Browser(location, document, head, XHR, $log) {
// Misc API
//////////////////////////////////////////////////////////////
var hoverListener = noop;
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#hover
* @methodOf angular.service.$browser
*
* @description
* Set hover listener.
*
* @param {function(Object, boolean)} listener Function that will be called when hover event
* occurs.
*/
self.hover = function(listener) { hoverListener = listener; };
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#bind
* @methodOf angular.service.$browser
*
* @description
* Register hover function to real browser
*/
self.bind = function() {
document.bind("mouseover", function(event){
hoverListener(jqLite(msie ? event.srcElement : event.target), true);
@@ -189,9 +291,16 @@ function Browser(location, document, head, XHR, $log) {
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addCss
* @methodOf angular.service.$browser
*
* @param {string} url Url to css file
* @description
* Adds a stylesheet tag to the head.
*/
self.addCss = function(/**string*/url) {
self.addCss = function(url) {
var link = jqLite(rawDocument.createElement('link'));
link.attr('rel', 'stylesheet');
link.attr('type', 'text/css');
@@ -201,9 +310,18 @@ function Browser(location, document, head, XHR, $log) {
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addJs
* @methodOf angular.service.$browser
*
* @param {string} url Url to js file
* @param {string=} dom_id Optional id for the script tag
*
* @description
* Adds a script tag to the head.
*/
self.addJs = function(/**string*/url, /**string*/dom_id) {
self.addJs = function(url, dom_id) {
var script = jqLite(rawDocument.createElement('script'));
script.attr('type', 'text/javascript');
script.attr('src', url);
+63 -2
View File
@@ -109,12 +109,66 @@ Compiler.prototype = {
};
},
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:eval-order
*
* @description
* Normally the view is updated from top to bottom. This usually is
* not a problem, but under some circumstances the values for data
* is not available until after the full view is computed. If such
* values are needed before they are computed the order of
* evaluation can be change using ng:eval-order
*
* @element ANY
* @param {integer|string=} [priority=0] priority integer, or FIRST, LAST constant
*
* @exampleDescription
* try changing the invoice and see that the Total will lag in evaluation
* @example
<div>TOTAL: without ng:eval-order {{ items.$sum('total') | currency }}</div>
<div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}</div>
<table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}]">
<tr>
<td>QTY</td>
<td>Description</td>
<td>Cost</td>
<td>Total</td>
<td></td>
</tr>
<tr ng:repeat="item in items">
<td><input name="item.qty"/></td>
<td><input name="item.desc"/></td>
<td><input name="item.cost"/></td>
<td>{{item.total = item.qty * item.cost | currency}}</td>
<td><a href="" ng:click="items.$remove(item)">X</a></td>
</tr>
<tr>
<td colspan="3"><a href="" ng:click="items.$add()">add</a></td>
<td>{{ items.$sum('total') | currency }}</td>
</tr>
</table>
*
* @scenario
it('should check ng:format', function(){
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$9.99');
input('item.qty').enter('2');
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$19.98');
});
*/
templatize: function(element, elementIndex, priority){
var self = this,
widget,
fn,
directiveFns = self.directives,
descend = true,
directives = true,
elementName = nodeName(element),
template,
selfApi = {
compile: bind(self, self.compile),
@@ -138,12 +192,15 @@ Compiler.prototype = {
eachAttribute(element, function(value, name){
if (!widget) {
if (widget = self.widgets('@' + name)) {
element.addClass('ng-attr-widget');
widget = bind(selfApi, widget, value, element);
}
}
});
if (!widget) {
if (widget = self.widgets(nodeName(element))) {
if (widget = self.widgets(elementName)) {
if (elementName.indexOf(':') > 0)
element.addClass('ng-widget');
widget = bind(selfApi, widget, element);
}
}
@@ -179,7 +236,11 @@ Compiler.prototype = {
});
});
eachAttribute(element, function(value, name){
template.addInit((directiveFns[name]||noop).call(selfApi, value, element));
fn = directiveFns[name];
if (fn) {
element.addClass('ng-directive');
template.addInit((directiveFns[name]).call(selfApi, value, element));
}
});
}
// Process non text child nodes
+3 -2
View File
@@ -19,8 +19,9 @@ function createInjector(providerScope, providers, cache) {
* string: return an instance for the injection key.
* array of keys: returns an array of instances.
* function: look at $inject property of function to determine instances
* and then call the function with instances and scope. Any
* additional arguments are passed on to function.
* and then call the function with instances and `scope`. Any
* additional arguments (`args`) are appended to the function
* arguments.
* object: initialize eager providers and publish them the ones with publish here.
* none: same as object but use providerScope as place to publish.
*/
+51 -15
View File
@@ -1,11 +1,36 @@
var array = [].constructor;
function toJson(obj, pretty){
/**
* @workInProgress
* @ngdoc function
* @name angular.toJson
* @function
*
* @description
* Serializes the input into a JSON formated string.
*
* @param {Object|Array|Date|string|number} obj Input to jsonify.
* @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace.
* @returns {string} Jsonified string representing `obj`.
*/
function toJson(obj, pretty) {
var buf = [];
toJsonArray(buf, obj, pretty ? "\n " : _null, []);
return buf.join('');
}
/**
* @workInProgress
* @ngdoc function
* @name angular.fromJson
* @function
*
* @description
* Deserializes a string in the JSON format.
*
* @param {string} json JSON string to deserialize.
* @returns {Object|Array|Date|string|number} Deserialized thingy.
*/
function fromJson(json) {
if (!json) return json;
try {
@@ -22,38 +47,49 @@ function fromJson(json) {
angular['toJson'] = toJson;
angular['fromJson'] = fromJson;
function toJsonArray(buf, obj, pretty, stack){
if (typeof obj == "object") {
function toJsonArray(buf, obj, pretty, stack) {
if (isObject(obj)) {
if (obj === window) {
buf.push('WINDOW');
return;
}
if (obj === document) {
buf.push('DOCUMENT');
return;
}
if (includes(stack, obj)) {
buf.push("RECURSION");
buf.push('RECURSION');
return;
}
stack.push(obj);
}
var type = typeof obj;
if (obj === _null) {
buf.push($null);
} else if (type === $function) {
} else if (obj instanceof RegExp) {
buf.push(angular['String']['quoteUnicode'](obj.toString()));
} else if (isFunction(obj)) {
return;
} else if (type === $boolean) {
} else if (isBoolean(obj)) {
buf.push('' + obj);
} else if (type === $number) {
} else if (isNumber(obj)) {
if (isNaN(obj)) {
buf.push($null);
} else {
buf.push('' + obj);
}
} else if (type === $string) {
} else if (isString(obj)) {
return buf.push(angular['String']['quoteUnicode'](obj));
} else if (type === $object) {
if (obj instanceof Array) {
} else if (isObject(obj)) {
if (isArray(obj)) {
buf.push("[");
var len = obj.length;
var sep = false;
for(var i=0; i<len; i++) {
var item = obj[i];
if (sep) buf.push(",");
if (typeof item == $function || typeof item == $undefined) {
if (!(item instanceof RegExp) && (isFunction(item) || isUndefined(item))) {
buf.push($null);
} else {
toJsonArray(buf, item, pretty, stack);
@@ -61,7 +97,7 @@ function toJsonArray(buf, obj, pretty, stack){
sep = true;
}
buf.push("]");
} else if (obj instanceof Date) {
} else if (isDate(obj)) {
buf.push(angular['String']['quoteUnicode'](angular['Date']['toString'](obj)));
} else {
buf.push("{");
@@ -70,7 +106,7 @@ function toJsonArray(buf, obj, pretty, stack){
var childPretty = pretty ? pretty + " " : false;
var keys = [];
for(var k in obj) {
if (k.indexOf('$') === 0 || obj[k] === _undefined)
if (obj[k] === _undefined)
continue;
keys.push(k);
}
@@ -92,7 +128,7 @@ function toJsonArray(buf, obj, pretty, stack){
buf.push("}");
}
}
if (typeof obj == $object) {
if (isObject(obj)) {
stack.pop();
}
}
+353 -14
View File
@@ -49,11 +49,11 @@ var scopeId = 0,
compileCache = {},
JS_KEYWORDS = {};
foreach(
["abstract", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default",
"delete", "do", "double", "else", "enum", "export", "extends", "false", "final", "finally", "float", "for", $function, "goto",
"if", "implements", "import", "ininstanceof", "intinterface", "long", "native", "new", $null, "package", "private",
"protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws",
"transient", "true", "try", "typeof", "var", "volatile", "void", $undefined, "while", "with"],
("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," +
"delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," +
"if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," +
"protected,public,return,short,static,super,switch,synchronized,this,throw,throws," +
"transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/),
function(key){ JS_KEYWORDS[key] = true;}
);
function getterFn(path){
@@ -101,9 +101,134 @@ function expressionCompile(exp){
}
function errorHandlerFor(element, error) {
elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error);
elementError(element, NG_EXCEPTION, isDefined(error) ? formatError(error) : error);
}
/**
* @workInProgress
* @ngdoc overview
* @name angular.scope
*
* @description
* Scope is a JavaScript object and the execution context for expressions. You can think about
* scopes as JavaScript objects that have extra APIs for registering watchers. A scope is the model
* in the model-view-controller design pattern.
*
* A few other characteristics of scopes:
*
* - Scopes can be nested. A scope (prototypically) inherits properties from its parent scope.
* - Scopes can be attached (bound) to the HTML DOM tree (the view).
* - A scope {@link angular.scope.$become becomes} `this` for a controller.
* - Scope's {@link angular.scope.$eval $eval} is used to update its view.
* - Scopes can {@link angular.scope.$watch watch} properties and fire events.
*
* # Basic Operations
* Scopes can be created by calling {@link angular.scope() angular.scope()} or by compiling HTML.
*
* {@link angular.widget Widgets} and data bindings register listeners on the current scope to get
* notified of changes to the scope state. When notified, these listeners push the updated state
* through to the DOM.
*
* Here is a simple scope snippet to show how you can interact with the scope.
* <pre>
var scope = angular.scope();
scope.salutation = 'Hello';
scope.name = 'World';
expect(scope.greeting).toEqual(undefined);
scope.$watch('name', function(){
this.greeting = this.salutation + ' ' + this.name + '!';
});
expect(scope.greeting).toEqual('Hello World!');
scope.name = 'Misko';
// scope.$eval() will propagate the change to listeners
expect(scope.greeting).toEqual('Hello World!');
scope.$eval();
expect(scope.greeting).toEqual('Hello Misko!');
* </pre>
*
* # Inheritance
* A scope can inherit from a parent scope, as in this example:
* <pre>
var parent = angular.scope();
var child = angular.scope(parent);
parent.salutation = "Hello";
child.name = "World";
expect(child.salutation).toEqual('Hello');
child.salutation = "Welcome";
expect(child.salutation).toEqual('Welcome');
expect(parent.salutation).toEqual('Hello');
* </pre>
*
* # Dependency Injection
* Scope also acts as a simple dependency injection framework.
*
* **TODO**: more info needed
*
* # When scopes are evaluated
* Anyone can update a scope by calling its {@link angular.scope.$eval $eval()} method. By default
* angular widgets listen to user change events (e.g. the user enters text into text field), copy
* the data from the widget to the scope (the MVC model), and then call the `$eval()` method on the
* root scope to update dependents. This creates a spreadsheet-like behavior: the bound views update
* immediately as the user types into the text field.
*
* Similarly, when a request to fetch data from a server is made and the response comes back, the
* data is written into the model and then $eval() is called to push updates through to the view and
* any other dependents.
*
* Because a change in the model that's triggered either by user input or by server response calls
* `$eval()`, it is unnecessary to call `$eval()` from within your controller. The only time when
* calling `$eval()` is needed, is when implementing a custom widget or service.
*
* Because scopes are inherited, the child scope `$eval()` overrides the parent `$eval()` method.
* So to update the whole page you need to call `$eval()` on the root scope as `$root.$eval()`.
*
* Note: A widget that creates scopes (i.e. {@link angular.widget.@ng:repeat ng:repeat}) is
* responsible for forwarding `$eval()` calls from the parent to those child scopes. That way,
* calling $eval() on the root scope will update the whole page.
*
* @exampleDescription
* This example demonstrates scope inheritance and property overriding.
*
* In this example, the root scope encompasses the whole HTML DOM tree. This scope has `salutation`,
* `name`, and `names` properties. The {@link angular.widget@ng:repeat ng:repeat} creates a child
* scope, one for each element in the names array. The repeater also assigns $index and name into
* the child scope.
*
* Notice that:
*
* - While the name is set in the child scope it does not change the name defined in the root scope.
* - The child scope inherits the salutation property from the root scope.
* - The $index property does not leak from the child scope to the root scope.
*
* @example
<ul ng:init="salutation='Hello'; name='Misko'; names=['World', 'Earth']">
<li ng:repeat="name in names">
{{$index}}: {{salutation}} {{name}}!
</li>
</ul>
<pre>
$index={{$index}}
salutation={{salutation}}
name={{name}}</pre>
@scenario
it('should inherit the salutation property and override the name property', function() {
expect(using('.doc-example-live').repeater('li').row(0)).
toEqual(['0', 'Hello', 'World']);
expect(using('.doc-example-live').repeater('li').row(1)).
toEqual(['1', 'Hello', 'Earth']);
expect(using('.doc-example-live').element('pre').text()).
toBe('$index=\nsalutation=Hello\nname=Misko');
});
*/
function createScope(parent, providers, instanceCache) {
function Parent(){}
parent = Parent.prototype = (parent || {});
@@ -115,11 +240,64 @@ function createScope(parent, providers, instanceCache) {
'this': instance,
$id: (scopeId++),
$parent: parent,
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$bind
* @function
*
* @description
* Binds a function `fn` to the current scope. See: {@link angular.bind}.
<pre>
var scope = angular.scope();
var fn = scope.$bind(function(){
return this;
});
expect(fn()).toEqual(scope);
</pre>
*
* @param {function()} fn Function to be bound.
*/
$bind: bind(instance, bind, instance),
$get: bind(instance, getter, instance),
$set: bind(instance, setter, instance),
$eval: function $eval(exp) {
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$eval
* @function
*
* @description
* Without the `exp` parameter triggers an eval cycle, for this scope and it's child scopes.
*
* With the `exp` parameter, compiles the expression to a function and calls it with `this` set
* to the current scope and returns the result.
*
* # Example
<pre>
var scope = angular.scope();
scope.a = 1;
scope.b = 2;
expect(scope.$eval('a+b')).toEqual(3);
expect(scope.$eval(function(){ return this.a + this.b; })).toEqual(3);
scope.$onEval('sum = a+b');
expect(scope.sum).toEqual(undefined);
scope.$eval();
expect(scope.sum).toEqual(3);
</pre>
*
* @param {(string|function())=} exp An angular expression to be compiled to a function or a js
* function.
*
* @returns {*} The result of calling compiled `exp` with `this` set to the current scope.
*/
$eval: function(exp) {
var type = typeof exp;
var i, iSize;
var j, jSize;
@@ -145,6 +323,44 @@ function createScope(parent, providers, instanceCache) {
}
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$tryEval
* @function
*
* @description
* Evaluates the expression in the context of the current scope just like
* {@link angular.scope.$eval()} with expression parameter, but also wraps it in a try/catch
* block.
*
* If exception is thrown then `exceptionHandler` is used to handle the exception.
*
* # Example
<pre>
var scope = angular.scope();
scope.error = function(){ throw 'myerror'; };
scope.$exceptionHandler = function(e) {this.lastException = e; };
expect(scope.$eval('error()'));
expect(scope.lastException).toEqual('myerror');
this.lastException = null;
expect(scope.$eval('error()'), function(e) {this.lastException = e; });
expect(scope.lastException).toEqual('myerror');
var body = angular.element(window.document.body);
expect(scope.$eval('error()'), body);
expect(body.attr('ng-exception')).toEqual('"myerror"');
expect(body.hasClass('ng-exception')).toEqual(true);
</pre>
*
* @param {string|function()} expression Angular expression to evaluate.
* @param {function()|DOMElement} exceptionHandler Function to be called or DOMElement to be
* decorated.
* @returns {*} The result of `expression` evaluation.
*/
$tryEval: function (expression, exceptionHandler) {
var type = typeof expression;
try {
@@ -165,14 +381,58 @@ function createScope(parent, providers, instanceCache) {
}
},
$watch: function(watchExp, listener, exceptionHandler) {
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$watch
* @function
*
* @description
* Registers `listener` as a callback to be executed every time the `watchExp` changes. Be aware
* that callback gets, by default, called upon registration, this can be prevented via the
* `initRun` parameter.
*
* # Example
<pre>
var scope = angular.scope();
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', 'counter = counter + 1');
expect(scope.counter).toEqual(1);
scope.$eval();
expect(scope.counter).toEqual(1);
scope.name = 'adam';
scope.$eval();
expect(scope.counter).toEqual(2);
</pre>
*
* @param {function()|string} watchExp Expression that should be evaluated and checked for
* change during each eval cycle. Can be an angular string expression or a function.
* @param {function()|string} listener Function (or angular string expression) that gets called
* every time the value of the `watchExp` changes. The function will be called with two
* parameters, `newValue` and `oldValue`.
* @param {(function()|DOMElement)=} [exceptionHanlder=angular.service.$exceptionHandler] Handler
* that gets called when `watchExp` or `listener` throws an exception. If a DOMElement is
* specified as handler, the element gets decorated by angular with the information about the
* exception.
* @param {boolean=} [initRun=true] Flag that prevents the first execution of the listener upon
* registration.
*
*/
$watch: function(watchExp, listener, exceptionHandler, initRun) {
var watch = expressionCompile(watchExp),
last;
last = watch.call(instance);
listener = expressionCompile(listener);
function watcher(){
function watcher(firstRun){
var value = watch.call(instance),
// we have to save the value because listener can call ourselves => inf loop
lastValue = last;
if (last !== value) {
if (firstRun || lastValue !== value) {
last = value;
instance.$tryEval(function(){
return listener.call(instance, value, lastValue);
@@ -180,9 +440,36 @@ function createScope(parent, providers, instanceCache) {
}
}
instance.$onEval(PRIORITY_WATCH, watcher);
watcher();
if (isUndefined(initRun)) initRun = true;
if (initRun) watcher(true);
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$onEval
* @function
*
* @description
* Evaluates the `expr` expression in the context of the current scope during each
* {@link angular.scope.$eval eval cycle}.
*
* # Example
<pre>
var scope = angular.scope();
scope.counter = 0;
scope.$onEval('counter = counter + 1');
expect(scope.counter).toEqual(0);
scope.$eval();
expect(scope.counter).toEqual(1);
</pre>
*
* @param {number} [priority=0] Execution priority. Lower priority numbers get executed first.
* @param {string|function()} expr Angular expression or function to be executed.
* @param {(function()|DOMElement)=} [exceptionHandler=angular.service.$exceptionHandler] Handler
* function to call or DOM element to decorate when an exception occurs.
*
*/
$onEval: function(priority, expr, exceptionHandler){
if (!isNumber(priority)) {
exceptionHandler = expr;
@@ -202,6 +489,12 @@ function createScope(parent, providers, instanceCache) {
});
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$postEval
* @function
*/
$postEval: function(expr) {
if (expr) {
var fn = expressionCompile(expr);
@@ -216,6 +509,36 @@ function createScope(parent, providers, instanceCache) {
}
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$become
* @function
* @deprecated This method will be removed before 1.0
*
* @description
* Modifies the scope to act like an instance of the given class by:
*
* - copying the class's prototype methods
* - applying the class's initialization function to the scope instance (without using the new
* operator)
*
* That makes the scope be a `this` for the given class's methods — effectively an instance of
* the given class with additional (scope) stuff. A scope can later `$become` another class.
*
* `$become` gets used to make the current scope act like an instance of a controller class.
* This allows for use of a controller class in two ways.
*
* - as an ordinary JavaScript class for standalone testing, instantiated using the new
* operator, with no attached view.
* - as a controller for an angular model stored in a scope, "instantiated" by
* `scope.$become(ControllerClass)`.
*
* Either way, the controller's methods refer to the model variables like `this.name`. When
* stored in a scope, the model supports data binding. When bound to a view, {{name}} in the
* HTML template refers to the same variable.
*/
$become: function(Class) {
if (isFunction(Class)) {
instance.constructor = Class;
@@ -231,9 +554,25 @@ function createScope(parent, providers, instanceCache) {
}
},
$new: function(Class) {
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$new
* @function
*
* @description
* Creates a new {@link angular.scope scope}, that:
*
* - is a child of the current scope
* - will {@link angular.scope.$become $become} of type specified via `constructor`
*
* @param {function()} constructor Constructor function of the type the new scope should assume.
* @returns {Object} The newly created child scope.
*
*/
$new: function(constructor) {
var child = createScope(instance);
child.$become.apply(instance, concat([Class], arguments, 1));
child.$become.apply(instance, concat([constructor], arguments, 1));
instance.$onEval(child.$eval);
return child;
}
+1
View File
@@ -53,6 +53,7 @@
addScript("/parser.js");
addScript("/Resource.js");
addScript("/Browser.js");
addScript("/sanitizer.js");
addScript("/AngularPublic.js");
// Extension points
+22 -12
View File
@@ -4,7 +4,7 @@ var angularGlobal = {
var type = typeof obj;
if (type == $object) {
if (obj instanceof Array) return $array;
if (obj instanceof Date) return $date;
if (isDate(obj)) return $date;
if (obj.nodeType == 1) return $element;
}
return type;
@@ -177,6 +177,8 @@ var angularArray = {
}
};
var R_ISO8061_STR = /^(\d{4})-(\d\d)-(\d\d)(?:T(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3}))?)?)?Z)?$/
var angularString = {
'quote':function(string) {
return '"' + string.replace(/\\/g, '\\\\').
@@ -202,13 +204,18 @@ var angularString = {
}
return chars.join('');
},
/**
* Tries to convert input to date and if successful returns the date, otherwise returns the input.
* @param {string} string
* @return {(Date|string)}
*/
'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$/))){
if (isString(string) && (match = string.match(R_ISO8061_STR))){
var date = new Date(0);
date.setUTCFullYear(match[1], match[2] - 1, match[3]);
date.setUTCHours(match[4], match[5], match[6], 0);
date.setUTCHours(match[4]||0, match[5]||0, match[6]||0, match[7]||0);
return date;
}
return string;
@@ -217,14 +224,17 @@ var angularString = {
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' ;
return !date ?
date :
date.toISOString ?
date.toISOString() :
padNumber(date.getUTCFullYear(), 4) + '-' +
padNumber(date.getUTCMonth() + 1, 2) + '-' +
padNumber(date.getUTCDate(), 2) + 'T' +
padNumber(date.getUTCHours(), 2) + ':' +
padNumber(date.getUTCMinutes(), 2) + ':' +
padNumber(date.getUTCSeconds(), 2) + '.' +
padNumber(date.getUTCMilliseconds(), 3) + 'Z';
}
};
+547 -65
View File
@@ -1,9 +1,99 @@
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:init
*
* @description
* `ng:init` attribute allows the for initialization tasks to be executed
* before the template enters execution mode during bootstrap.
*
* @element ANY
* @param {expression} expression to eval.
*
* @example
<div ng:init="greeting='Hello'; person='World'">
{{greeting}} {{person}}!
</div>
*
* @scenario
it('should check greeting', function(){
expect(binding('greeting')).toBe('Hello');
expect(binding('person')).toBe('World');
});
*/
angularDirective("ng:init", function(expression){
return function(element){
this.$tryEval(expression, element);
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:controller
*
* @description
* To support the Model-View-Controller design pattern, it is possible
* to assign behavior to a scope through `ng:controller`. The scope is
* the MVC model. The HTML (with data bindings) is the MVC view.
* The `ng:controller` directive specifies the MVC controller class
*
* @element ANY
* @param {expression} expression to eval.
*
* @example
<script type="text/javascript">
function SettingsController() {
this.name = "John Smith";
this.contacts = [
{type:'phone', value:'408 555 1212'},
{type:'email', value:'john.smith@example.org'} ];
}
SettingsController.prototype = {
greet: function(){
alert(this.name);
},
addContact: function(){
this.contacts.push({type:'email', value:'yourname@example.org'});
},
removeContact: function(contactToRemove) {
angular.Array.remove(this.contacts, contactToRemove);
},
clearContact: function(contact) {
contact.type = 'phone';
contact.value = '';
}
};
</script>
<div ng:controller="SettingsController">
Name: <input type="text" name="name"/>
[ <a href="" ng:click="greet()">greet</a> ]<br/>
Contact:
<ul>
<li ng:repeat="contact in contacts">
<select name="contact.type">
<option>phone</option>
<option>email</option>
</select>
<input type="text" name="contact.value"/>
[ <a href="" ng:click="clearContact(contact)">clear</a>
| <a href="" ng:click="removeContact(contact)">X</a> ]
</li>
<li>[ <a href="" ng:click="addContact()">add</a> ]</li>
</ul>
</div>
*
* @scenario
it('should check controller', function(){
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
expect(element('.doc-example-live li[ng\\:repeat-index="0"] input').val()).toBe('408 555 1212');
expect(element('.doc-example-live li[ng\\:repeat-index="1"] input').val()).toBe('john.smith@example.org');
element('.doc-example-live li:first a:contains("clear")').click();
expect(element('.doc-example-live li:first input').val()).toBe('');
element('.doc-example-live li:last a:contains("add")').click();
expect(element('.doc-example-live li[ng\\:repeat-index="2"] input').val()).toBe('yourname@example.org');
});
*/
angularDirective("ng:controller", function(expression){
this.scope(true);
return function(element){
@@ -16,25 +106,95 @@ angularDirective("ng:controller", function(expression){
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:eval
*
* @description
* The `ng:eval` allows you to execute a binding which has side effects
* without displaying the result to the user.
*
* @element ANY
* @param {expression} expression to eval.
*
* @exampleDescription
* Notice that `{{` `obj.multiplied = obj.a * obj.b` `}}` has a side effect of assigning
* a value to `obj.multiplied` and displaying the result to the user. Sometimes,
* however, it is desirable to execute a side effect without showing the value to
* the user. In such a case `ng:eval` allows you to execute code without updating
* the display.
*
* @example
* <input name="obj.a" value="6" >
* * <input name="obj.b" value="2">
* = {{obj.multiplied = obj.a * obj.b}} <br>
* <span ng:eval="obj.divide = obj.a / obj.b"></span>
* <span ng:eval="obj.updateCount = 1 + (obj.updateCount||0)"></span>
* <tt>obj.divide = {{obj.divide}}</tt><br/>
* <tt>obj.updateCount = {{obj.updateCount}}</tt>
*
* @scenario
it('should check eval', function(){
expect(binding('obj.divide')).toBe('3');
expect(binding('obj.updateCount')).toBe('2');
input('obj.a').enter('12');
expect(binding('obj.divide')).toBe('6');
expect(binding('obj.updateCount')).toBe('3');
});
*/
angularDirective("ng:eval", function(expression){
return function(element){
this.$onEval(expression, element);
};
});
angularDirective("ng:bind", function(expression){
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:bind
*
* @description
* The `ng:bind` attribute asks <angular/> to replace the text content of this
* HTML element with the value of the given expression and kept it up to
* date when the expression's value changes. Usually you just write
* {{expression}} and let <angular/> compile it into
* `<span ng:bind="expression"></span>` at bootstrap time.
*
* @element ANY
* @param {expression} expression to eval.
*
* @exampleDescription
* Try it here: enter text in text box and watch the greeting change.
* @example
* Enter name: <input type="text" name="name" value="Whirled">. <br>
* Hello <span ng:bind="name" />!
*
* @scenario
it('should check ng:bind', function(){
expect(using('.doc-example-live').binding('name')).toBe('Whirled');
using('.doc-example-live').input('name').enter('world');
expect(using('.doc-example-live').binding('name')).toBe('world');
});
*/
angularDirective("ng:bind", function(expression, element){
element.addClass('ng-binding');
return function(element) {
var lastValue = noop, lastError = noop;
this.$onEval(function() {
var error, value, isHtml, isDomElement,
var error, value, html, isHtml, isDomElement,
oldElement = this.hasOwnProperty($$element) ? this.$element : _undefined;
this.$element = element;
value = this.$tryEval(expression, function(e){
error = toJson(e);
error = formatError(e);
});
this.$element = oldElement;
// If we are HTML than save the raw HTML data so that we don't
// recompute sanitization since it is expensive.
// TODO: turn this into a more generic way to compute this
if (isHtml = (value instanceof HTML))
value = (html = value).html;
if (lastValue === value && lastError == error) return;
isHtml = value instanceof HTML;
isDomElement = isElement(value);
if (!isHtml && !isDomElement && isObject(value)) {
value = toJson(value);
@@ -45,7 +205,7 @@ angularDirective("ng:bind", function(expression){
elementError(element, NG_EXCEPTION, error);
if (error) value = error;
if (isHtml) {
element.html(value.html);
element.html(html.get());
} else if (isDomElement) {
element.html('');
element.append(value);
@@ -93,7 +253,41 @@ function compileBindTemplate(template){
return fn;
}
angularDirective("ng:bind-template", function(expression){
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:bind-template
*
* @description
* The `ng:bind-template` attribute specifies that the element
* text should be replaced with the template in ng:bind-template.
* Unlike ng:bind the ng:bind-template can contain multiple `{{` `}}`
* expressions. (This is required since some HTML elements
* can not have SPAN elements such as TITLE, or OPTION to name a few.
*
* @element ANY
* @param {string} template of form
* <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval.
*
* @exampleDescription
* Try it here: enter text in text box and watch the greeting change.
* @example
Salutation: <input type="text" name="salutation" value="Hello"><br/>
Name: <input type="text" name="name" value="World"><br/>
<pre ng:bind-template="{{salutation}} {{name}}!"></pre>
*
* @scenario
it('should check ng:bind', function(){
expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
toBe('Hello World!');
using('.doc-example-live').input('salutation').enter('Greetings');
using('.doc-example-live').input('name').enter('user');
expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
toBe('Greetings user!');
});
*/
angularDirective("ng:bind-template", function(expression, element){
element.addClass('ng-binding');
var templateFn = compileBindTemplate(expression);
return function(element) {
var lastValue;
@@ -112,6 +306,56 @@ var REMOVE_ATTRIBUTES = {
'readonly':'readOnly',
'checked':'checked'
};
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:bind-attr
*
* @description
* The `ng:bind-attr` attribute specifies that the element attributes
* which should be replaced by the expression in it. Unlike `ng:bind`
* the `ng:bind-attr` contains a JSON key value pairs representing
* which attributes need to be changed. You dont usually write the
* `ng:bind-attr` in the HTML since embedding
* <tt ng:non-bindable>{{expression}}</tt> into the
* attribute directly is the preferred way. The attributes get
* translated into `<span ng:bind-attr="{attr:expression}"/>` at
* bootstrap time.
*
* This HTML snippet is preferred way of working with `ng:bind-attr`
* <pre>
* <a href="http://www.google.com/search?q={{query}}">Google</a>
* </pre>
*
* The above gets translated to bellow during bootstrap time.
* <pre>
* <a ng:bind-attr='{"href":"http://www.google.com/search?q={{query}}"}'>Google</a>
* </pre>
*
* @element ANY
* @param {string} attribute_json a JSON key-value pairs representing
* the attributes to replace. Each key matches the attribute
* which needs to be replaced. Each value is a text template of
* the attribute with embedded
* <tt ng:non-bindable>{{expression}}</tt>s. Any number of
* key-value pairs can be specified.
*
* @exampleDescription
* Try it here: enter text in text box and click Google.
* @example
Google for:
<input type="text" name="query" value="AngularJS"/>
<a href="http://www.google.com/search?q={{query}}">Google</a>
*
* @scenario
it('should check ng:bind-attr', function(){
expect(using('.doc-example-live').element('a').attr('href')).
toBe('http://www.google.com/search?q=AngularJS');
using('.doc-example-live').input('query').enter('google');
expect(using('.doc-example-live').element('a').attr('href')).
toBe('http://www.google.com/search?q=google');
});
*/
angularDirective("ng:bind-attr", function(expression){
return function(element){
var lastValue = {};
@@ -140,72 +384,38 @@ angularDirective("ng:bind-attr", function(expression){
};
});
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];
var children = [], currentScope = this;
this.$onEval(function(){
var index = 0, childCount = children.length, childScope, lastElement = reference,
collection = this.$tryEval(rhs, reference), is_array = isArray(collection);
for ( var key in collection) {
if (!is_array || collection.hasOwnProperty(key)) {
if (index < childCount) {
// reuse existing child
childScope = children[index];
childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key;
} else {
// grow children
childScope = template(quickClone(element), 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);
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:click
*
* @description
* The ng:click allows you to specify custom behavior when
* element is clicked.
*
* @element ANY
* @param {expression} expression to eval upon click.
*
* @example
<button ng:click="count = count + 1" ng:init="count=0">
Increment
</button>
count: {{count}}
* @scenario
it('should check ng:click', function(){
expect(binding('count')).toBe('0');
element('.doc-example-live :button').click();
expect(binding('count')).toBe('1');
});
*/
/*
* A directive that allows creation of custom onclick handlers that are defined as angular
* expressions and are compiled and executed within the current scope.
*
* Events that are handled via these handler are always configured not to propagate further.
*
* TODO: maybe we should consider allowing users to control even propagation in the future.
* TODO: maybe we should consider allowing users to control event propagation in the future.
*/
angularDirective("ng:click", function(expression, element){
return function(element){
@@ -218,6 +428,83 @@ angularDirective("ng:click", function(expression, element){
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:submit
*
* @description
*
* @element form
* @param {expression} expression to eval.
*
* @exampleDescription
* @example
* <form ng:submit="list.push(text);text='';" ng:init="list=[]">
* Enter text and hit enter:
* <input type="text" name="text" value="hello"/>
* </form>
* <pre>list={{list}}</pre>
* @scenario
it('should check ng:submit', function(){
expect(binding('list')).toBe('list=[]');
element('.doc-example-live form input').click();
this.addFutureAction('submit from', function($window, $document, done) {
$window.angular.element(
$document.elements('.doc-example-live form')).
trigger('submit');
done();
});
expect(binding('list')).toBe('list=["hello"]');
});
*/
/**
* Enables binding angular expressions to onsubmit events.
*
* Additionally it prevents the default action (which for form means sending the request to the
* server and reloading the current page).
*/
angularDirective("ng:submit", function(expression, element) {
return function(element) {
var self = this;
element.bind('submit', function(event) {
self.$tryEval(expression, element);
self.$root.$eval();
event.preventDefault();
});
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:watch
*
* @description
* The `ng:watch` allows you watch a variable and then execute
* an evaluation on variable change.
*
* @element ANY
* @param {expression} expression to eval.
*
* @exampleDescription
* Notice that the counter is incremented
* every time you change the text.
* @example
<div ng:init="counter=0" ng:watch="name: counter = counter+1">
<input type="text" name="name" value="hello"><br/>
Change counter: {{counter}} Name: {{name}}
</div>
* @scenario
it('should check ng:watch', function(){
expect(using('.doc-example-live').binding('counter')).toBe('2');
using('.doc-example-live').input('name').enter('abc');
expect(using('.doc-example-live').binding('counter')).toBe('3');
});
*/
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
angularDirective("ng:watch", function(expression, element){
return function(element){
var self = this;
@@ -246,10 +533,145 @@ function ngClass(selector) {
};
}
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:class
*
* @description
* The `ng:class` allows you to set CSS class on HTML element
* conditionally.
*
* @element ANY
* @param {expression} expression to eval.
*
* @exampleDescription
* @example
<input type="button" value="set" ng:click="myVar='ng-input-indicator-wait'">
<input type="button" value="clear" ng:click="myVar=''">
<br>
<span ng:class="myVar">Sample Text &nbsp;&nbsp;&nbsp;&nbsp;</span>
*
* @scenario
it('should check ng:class', function(){
expect(element('.doc-example-live span').attr('className')).not().
toMatch(/ng-input-indicator-wait/);
using('.doc-example-live').element(':button:first').click();
expect(element('.doc-example-live span').attr('className')).
toMatch(/ng-input-indicator-wait/);
using('.doc-example-live').element(':button:last').click();
expect(element('.doc-example-live span').attr('className')).not().
toMatch(/ng-input-indicator-wait/);
});
*/
angularDirective("ng:class", ngClass(function(){return true;}));
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:class-odd
*
* @description
* The `ng:class-odd` and `ng:class-even` works exactly as
* `ng:class`, except it works in conjunction with `ng:repeat`
* and takes affect only on odd (even) rows.
*
* @element ANY
* @param {expression} expression to eval. Must be inside
* `ng:repeat`.
*
* @exampleDescription
* @example
<ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
<li ng:repeat="name in names">
<span ng:class-odd="'ng-format-negative'"
ng:class-even="'ng-input-indicator-wait'">
{{name}} &nbsp; &nbsp; &nbsp;
</span>
</li>
</ol>
*
* @scenario
it('should check ng:class-odd and ng:class-even', function(){
expect(element('.doc-example-live li:first span').attr('className')).
toMatch(/ng-format-negative/);
expect(element('.doc-example-live li:last span').attr('className')).
toMatch(/ng-input-indicator-wait/);
});
*/
angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:class-even
*
* @description
* The `ng:class-odd` and `ng:class-even` works exactly as
* `ng:class`, except it works in conjunction with `ng:repeat`
* and takes affect only on odd (even) rows.
*
* @element ANY
* @param {expression} expression to eval. Must be inside
* `ng:repeat`.
*
* @exampleDescription
* @example
<ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
<li ng:repeat="name in names">
<span ng:class-odd="'ng-format-negative'"
ng:class-even="'ng-input-indicator-wait'">
{{name}} &nbsp; &nbsp; &nbsp;
</span>
</li>
</ol>
*
* @scenario
it('should check ng:class-odd and ng:class-even', function(){
expect(element('.doc-example-live li:first span').attr('className')).
toMatch(/ng-format-negative/);
expect(element('.doc-example-live li:last span').attr('className')).
toMatch(/ng-input-indicator-wait/);
});
*/
angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:show
*
* @description
* The `ng:show` and `ng:hide` allows you to show or hide a portion
* of the HTML conditionally.
*
* @element ANY
* @param {expression} expression if truthy then the element is
* shown or hidden respectively.
*
* @exampleDescription
* @example
Click me: <input type="checkbox" name="checked"><br/>
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
*
* @scenario
it('should check ng:show / ng:hide', function(){
expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
input('checked').check();
expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
});
*/
angularDirective("ng:show", function(expression, element){
return function(element){
this.$onEval(function(){
@@ -258,6 +680,36 @@ angularDirective("ng:show", function(expression, element){
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:hide
*
* @description
* The `ng:show` and `ng:hide` allows you to show or hide a portion
* of the HTML conditionally.
*
* @element ANY
* @param {expression} expression if truthy then the element is
* shown or hidden respectively.
*
* @exampleDescription
* @example
Click me: <input type="checkbox" name="checked"><br/>
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
*
* @scenario
it('should check ng:show / ng:hide', function(){
expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
input('checked').check();
expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
});
*/
angularDirective("ng:hide", function(expression, element){
return function(element){
this.$onEval(function(){
@@ -266,6 +718,36 @@ angularDirective("ng:hide", function(expression, element){
};
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:style
*
* @description
* The ng:style allows you to set CSS style on an HTML element conditionally.
*
* @element ANY
* @param {expression} expression which evals to an object whes key's are
* CSS style names and values are coresponding values for those
* CSS keys.
*
* @exampleDescription
* @example
<input type="button" value="set" ng:click="myStyle={color:'red'}">
<input type="button" value="clear" ng:click="myStyle={}">
<br/>
<span ng:style="myStyle">Sample Text</span>
<pre>myStyle={{myStyle}}</pre>
*
* @scenario
it('should check ng:style', function(){
expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
element('.doc-example-live :button[value=set]').click();
expect(element('.doc-example-live span').css('color')).toBe('red');
element('.doc-example-live :button[value=clear]').click();
expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
});
*/
angularDirective("ng:style", function(expression, element){
return function(element){
var resetStyle = getStyle(element);
+371 -23
View File
@@ -1,17 +1,82 @@
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.currency
* @function
*
* @description
* Formats a number as a currency (ie $1,234.56).
*
* @param {number} amount Input to filter.
* @returns {string} Formated number.
*
* @css ng-format-negative
* When the value is negative, this css class is applied to the binding making it by default red.
*
* @example
<input type="text" name="amount" value="1234.56"/> <br/>
{{amount | currency}}
*
* @scenario
it('should init with 1234.56', function(){
expect(binding('amount | currency')).toBe('$1,234.56');
});
it('should update', function(){
input('amount').enter('-1234');
expect(binding('amount | currency')).toBe('$-1,234.00');
expect(element('.doc-example-live .ng-binding').attr('className')).
toMatch(/ng-format-negative/);
});
*/
angularFilter.currency = function(amount){
this.$element.toggleClass('ng-format-negative', amount < 0);
return '$' + angularFilter['number'].apply(this, [amount, 2]);
};
angularFilter.number = function(amount, fractionSize){
if (isNaN(amount) || !isFinite(amount)) {
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.number
* @function
*
* @description
* Formats a number as text.
*
* If the input is not a number empty string is returned.
*
* @param {number|string} number Number to format.
* @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to.
* @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
*
* @example
Enter number: <input name='val' value='1234.56789' /><br/>
Default formatting: {{val | number}}<br/>
No fractions: {{val | number:0}}<br/>
Negative number: {{-val | number:4}}
* @scenario
it('should format numbers', function(){
expect(binding('val | number')).toBe('1,234.57');
expect(binding('val | number:0')).toBe('1,235');
expect(binding('-val | number:4')).toBe('-1,234.5679');
});
it('should update', function(){
input('val').enter('3374.333');
expect(binding('val | number')).toBe('3,374.33');
expect(binding('val | number:0')).toBe('3,374');
expect(binding('-val | number:4')).toBe('-3,374.3330');
});
*/
angularFilter.number = function(number, fractionSize){
if (isNaN(number) || !isFinite(number)) {
return '';
}
fractionSize = typeof fractionSize == $undefined ? 2 : fractionSize;
var isNegative = amount < 0;
amount = Math.abs(amount);
var isNegative = number < 0;
number = Math.abs(number);
var pow = Math.pow(10, fractionSize);
var text = "" + Math.round(amount * pow);
var text = "" + Math.round(number * pow);
var whole = text.substring(0, text.length - fractionSize);
whole = whole || '0';
var frc = text.substring(text.length - fractionSize);
@@ -30,6 +95,8 @@ angularFilter.number = function(amount, fractionSize){
}
return text;
};
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
@@ -42,15 +109,19 @@ function padNumber(num, digits, trim) {
num = num.substr(num.length - digits);
return neg + num;
}
function dateGetter(name, size, offset, trim) {
return function(date) {
var value = date['get' + name].call(date);
var value = date['get' + name]();
if (offset > 0 || value > -offset)
value += offset;
if (value === 0 && offset == -12 ) value = 12;
return padNumber(value, size, trim);
};
}
var DATE_FORMATS = {
yyyy: dateGetter('FullYear', 4),
yy: dateGetter('FullYear', 2, 0, true),
@@ -66,16 +137,83 @@ var DATE_FORMATS = {
m: dateGetter('Minutes', 1),
ss: dateGetter('Seconds', 2),
s: dateGetter('Seconds', 1),
a: function(date){return date.getHours() < 12 ? 'am' : 'pm'; },
a: function(date){return date.getHours() < 12 ? 'am' : 'pm';},
Z: function(date){
var offset = date.getTimezoneOffset();
return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2);
}
};
var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/;
var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/;
var NUMBER_STRING = /^\d+$/;
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.date
* @function
*
* @description
* Formats `date` to a string based on the requested `format`.
*
* `format` string can be composed of the following elements:
*
* * `'yyyy'`: 4 digit representation of year e.g. 2010
* * `'yy'`: 2 digit representation of year, padded (00-99)
* * `'MM'`: Month in year, padded (0112)
* * `'M'`: Month in year (112)
* * `'dd'`: Day in month, padded (0131)
* * `'d'`: Day in month (1-31)
* * `'HH'`: Hour in day, padded (0023)
* * `'H'`: Hour in day (0-23)
* * `'hh'`: Hour in am/pm, padded (0112)
* * `'h'`: Hour in am/pm, (1-12)
* * `'mm'`: Minute in hour, padded (0059)
* * `'m'`: Minute in hour (0-59)
* * `'ss'`: Second in minute, padded (0059)
* * `'s'`: Second in minute (059)
* * `'a'`: am/pm marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-12001200)
*
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ).
* @param {string=} format Formatting rules. If not specified, Date#toLocaleDateString is used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
<span ng:non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>:
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}<br/>
<span ng:non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}<br/>
*
* @scenario
it('should format date', function(){
expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).
toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/);
expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(am|pm)/);
});
*
*/
angularFilter.date = function(date, format) {
if (!(date instanceof Date)) return date;
if (isString(date)) {
if (NUMBER_STRING.test(date)) {
date = parseInt(date, 10);
} else {
date = angularString.toDate(date);
}
}
if (isNumber(date)) {
date = new Date(date);
}
if (!isDate(date)) {
return date;
}
var text = date.toLocaleDateString(), fn;
if (format && isString(format)) {
text = '';
@@ -92,37 +230,247 @@ angularFilter.date = function(date, format) {
return text;
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.json
* @function
*
* @description
* Allows you to convert a JavaScript object into JSON string.
*
* This filter is mostly useful for debugging. When using the double curly {{value}} notation
* the binding is automatically converted to JSON.
*
* @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
* @returns {string} JSON string.
*
* @css ng-monospace Always applied to the encapsulating element.
*
* @example:
<input type="text" name="objTxt" value="{a:1, b:[]}"
ng:eval="obj = $eval(objTxt)"/>
<pre>{{ obj | json }}</pre>
*
* @scenario
it('should jsonify filtered objects', function() {
expect(binding('obj | json')).toBe('{\n "a":1,\n "b":[]}');
});
it('should update', function() {
input('objTxt').enter('[1, 2, 3]');
expect(binding('obj | json')).toBe('[1,2,3]');
});
*
*/
angularFilter.json = function(object) {
this.$element.addClass("ng-monospace");
return toJson(object, true);
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.lowercase
* @function
*
* @see angular.lowercase
*/
angularFilter.lowercase = lowercase;
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.uppercase
* @function
*
* @see angular.uppercase
*/
angularFilter.uppercase = uppercase;
angularFilter.html = function(html){
return new HTML(html);
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.html
* @function
*
* @description
* Prevents the input from getting escaped by angular. By default the input is sanitized and
* inserted into the DOM as is.
*
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
* it into the returned string, however since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer.
*
* If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses
* the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this
* option is strongly discouraged and should be used only if you absolutely trust the input being
* filtered and you can't get the content through the sanitizer.
*
* @param {string} html Html input.
* @param {string=} option If 'unsafe' then do not sanitize the HTML input.
* @returns {string} Sanitized or raw html.
*
* @example
Snippet: <textarea name="snippet" cols="60" rows="3">
&lt;p style="color:blue"&gt;an html
&lt;em onmouseover="this.textContent='PWN3D!'"&gt;click here&lt;/em&gt;
snippet&lt;/p&gt;</textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="html-filter">
<td>html filter</td>
<td>
<pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
<div ng:bind="snippet | html"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet"></div></td>
</tr>
<tr id="html-unsafe-filter">
<td>unsafe html filter</td>
<td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
</tr>
</table>
*
* @scenario
it('should sanitize the html snippet ', function(){
expect(using('#html-filter').binding('snippet | html')).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it ('should escape snippet without any filter', function() {
expect(using('#escaped-html').binding('snippet')).
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
"snippet&lt;/p&gt;");
});
it ('should inline raw snippet if filtered as unsafe', function() {
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should update', function(){
input('snippet').enter('new <b>text</b>');
expect(using('#html-filter').binding('snippet | html')).toBe('new <b>text</b>');
expect(using('#escaped-html').binding('snippet')).toBe("new &lt;b&gt;text&lt;/b&gt;");
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new <b>text</b>');
});
*/
angularFilter.html = function(html, option){
return new HTML(html, option);
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.linky
* @function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plane email address links.
*
* @param {string} text Input text.
* @returns {string} Html-linkified text.
*
* @example
Snippet: <textarea name="snippet" cols="60" rows="3">
Pretty text with some links:
http://angularjs.org/,
mailto:us@somewhere.org,
another@somewhere.org,
and one more: ftp://127.0.0.1/.</textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="linky-filter">
<td>linky filter</td>
<td>
<pre>&lt;div ng:bind="snippet | linky"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
<div ng:bind="snippet | linky"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet"></div></td>
</tr>
</table>
@scenario
it('should linkify the snippet with urls', function(){
expect(using('#linky-filter').binding('snippet | linky')).
toBe('Pretty text with some links:\n' +
'<a href="http://angularjs.org/">http://angularjs.org/</a>,\n' +
'<a href="mailto:us@somewhere.org">us@somewhere.org</a>,\n' +
'<a href="mailto:another@somewhere.org">another@somewhere.org</a>,\n' +
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
});
it ('should not linkify snippet without the linky filter', function() {
expect(using('#escaped-html').binding('snippet')).
toBe("Pretty text with some links:\n" +
"http://angularjs.org/,\n" +
"mailto:us@somewhere.org,\n" +
"another@somewhere.org,\n" +
"and one more: ftp://127.0.0.1/.");
});
it('should update', function(){
input('snippet').enter('new http://link.');
expect(using('#linky-filter').binding('snippet | linky')).
toBe('new <a href="http://link">http://link</a>.');
expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
});
*/
//TODO: externalize all regexps
angularFilter.linky = function(text){
if (!text) return text;
function regExpEscape(text) {
return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1');
}
var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/;
var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/;
var match;
var raw = text;
var html = [];
var writer = htmlSanitizeWriter(html);
var url;
var i;
while (match=raw.match(URL)) {
var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,'');
var i = raw.indexOf(url);
html.push(escapeHtml(raw.substr(0, i)));
html.push('<a href="' + url + '">');
html.push(url);
html.push('</a>');
raw = raw.substring(i + url.length);
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2]==match[3]) url = 'mailto:' + url;
i = match.index;
writer.chars(raw.substr(0, i));
writer.start('a', {href:url});
writer.chars(match[0].replace(/^mailto:/, ''));
writer.end('a');
raw = raw.substring(i + match[0].length);
}
html.push(escapeHtml(raw));
writer.chars(raw);
return new HTML(html.join(''));
};
+123
View File
@@ -6,8 +6,79 @@ function toString(obj) {
var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
angularFormatter.noop = formatter(identity, identity);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.json
*
* @description
* Formats the user input as JSON text.
*
* @returns {string} A JSON string representation of the model.
*
* @example
* <div ng:init="data={name:'misko', project:'angular'}">
* <input type="text" size='50' name="data" ng:format="json"/>
* <pre>data={{data}}</pre>
* </div>
*
* @scenario
* it('should format json', function(){
* expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}');
* input('data').enter('{}');
* expect(binding('data')).toEqual('data={\n }');
* });
*/
angularFormatter.json = formatter(toJson, fromJson);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.boolean
*
* @description
* Use boolean formatter if you wish to store the data as boolean.
*
* @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`.
*
* @example
* Enter truthy text:
* <input type="text" name="value" ng:format="boolean" value="no"/>
* <input type="checkbox" name="value"/>
* <pre>value={{value}}</pre>
*
* @scenario
* it('should format boolean', function(){
* expect(binding('value')).toEqual('value=false');
* input('value').enter('truthy');
* expect(binding('value')).toEqual('value=true');
* });
*/
angularFormatter['boolean'] = formatter(toString, toBoolean);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.number
*
* @description
* Use number formatter if you wish to convert the user entered string to a number.
*
* @returns {number} Number from the parsed string.
*
* @example
* Enter valid number:
* <input type="text" name="value" ng:format="number" value="1234"/>
* <pre>value={{value}}</pre>
*
* @scenario
* it('should format numbers', function(){
* expect(binding('value')).toEqual('value=1234');
* input('value').enter('5678');
* expect(binding('value')).toEqual('value=5678');
* });
*/
angularFormatter.number = formatter(toString, function(obj){
if (obj == _null || NUMBER.exec(obj)) {
return obj===_null || obj === '' ? _null : 1*obj;
@@ -16,6 +87,32 @@ angularFormatter.number = formatter(toString, function(obj){
}
});
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.list
*
* @description
* Use list formatter if you wish to convert the user entered string to an array.
*
* @returns {Array} Array parsed from the entered string.
*
* @example
* Enter a list of items:
* <input type="text" name="value" ng:format="list" value=" chair ,, table"/>
* <input type="text" name="value" ng:format="list"/>
* <pre>value={{value}}</pre>
*
* @scenario
* it('should format lists', function(){
* expect(binding('value')).toEqual('value=["chair","table"]');
* this.addFutureAction('change to XYZ', function($window, $document, done){
* $document.elements('.doc-example :input:last').val(',,a,b,').trigger('change');
* done();
* });
* expect(binding('value')).toEqual('value=["a","b"]');
* });
*/
angularFormatter.list = formatter(
function(obj) { return obj ? obj.join(", ") : obj; },
function(value) {
@@ -28,6 +125,32 @@ angularFormatter.list = formatter(
}
);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.trim
*
* @description
* Use trim formatter if you wish to trim extra spaces in user text.
*
* @returns {String} Trim excess leading and trailing space.
*
* @example
* Enter text with leading/trailing spaces:
* <input type="text" name="value" ng:format="trim" value=" book "/>
* <input type="text" name="value" ng:format="trim"/>
* <pre>value={{value|json}}</pre>
*
* @scenario
* it('should format trim', function(){
* expect(binding('value')).toEqual('value="book"');
* this.addFutureAction('change to XYZ', function($window, $document, done){
* $document.elements('.doc-example :input:last').val(' text ').trigger('change');
* done();
* });
* expect(binding('value')).toEqual('value="text"');
* });
*/
angularFormatter.trim = formatter(
function(obj) { return obj ? trim("" + obj) : ""; }
);
+53
View File
@@ -68,6 +68,59 @@ angularTextMarkup('OPTION', function(text, textNode, parentElement){
}
});
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:href
*
* @description
* Using <angular/> markup like {{hash}} in an href attribute makes
* the page open to a wrong URL, ff the user clicks that link before
* angular has a chance to replace the {{hash}} with actual URL, the
* link will be broken and will most likely return a 404 error.
* The `ng:href` solves this problem by placing the `href` in the
* `ng:` namespace.
*
* The buggy way to write it:
* <pre>
* <a href="http://www.gravatar.com/avatar/{{hash}}"/>
* </pre>
*
* The correct way to write it:
* <pre>
* <a ng:href="http://www.gravatar.com/avatar/{{hash}}"/>
* </pre>
*
* @element ANY
* @param {template} template any string which can contain `{{}}` markup.
*/
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:src
*
* @description
* Using <angular/> markup like `{{hash}}` in a `src` attribute doesn't
* work right: The browser will fetch from the URL with the literal
* text `{{hash}}` until <angular/> replaces the expression inside
* `{{hash}}`. The `ng:src` attribute solves this problem by placing
* the `src` attribute in the `ng:` namespace.
*
* The buggy way to write it:
* <pre>
* <img src="http://www.gravatar.com/avatar/{{hash}}"/>
* </pre>
*
* The correct way to write it:
* <pre>
* <img ng:src="http://www.gravatar.com/avatar/{{hash}}"/>
* </pre>
*
* @element ANY
* @param {template} template any string which can contain `{{}}` markup.
*/
var NG_BIND_ATTR = 'ng:bind-attr';
var SPECIAL_ATTRS = {'ng:src': 'src', 'ng:href': 'href'};
angularAttrMarkup('{{}}', function(value, name, element){
+40 -59
View File
@@ -26,7 +26,7 @@ var OPERATORS = {
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
function lex(text, parseStringsForObjects){
var dateParseLength = parseStringsForObjects ? 20 : -1,
var dateParseLength = parseStringsForObjects ? 24 : -1,
tokens = [],
token,
index = 0,
@@ -67,10 +67,7 @@ function lex(text, parseStringsForObjects){
tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')});
index += 1;
} else {
throw "Lexer Error: Unexpected next character [" +
text.substring(index) +
"] in expression '" + text +
"' at column '" + (index+1) + "'.";
throwError("Unexpected next character ", index, index+1);
}
}
lastCh = ch;
@@ -93,7 +90,7 @@ function lex(text, parseStringsForObjects){
}
function isWhitespace(ch) {
return ch == ' ' || ch == '\r' || ch == '\t' ||
ch == '\n' || ch == '\v';
ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0
}
function isIdent(ch) {
return 'a' <= ch && ch <= 'z' ||
@@ -101,27 +98,37 @@ function lex(text, parseStringsForObjects){
'_' == ch || ch == '$';
}
function isExpOperator(ch) {
return ch == '-' || ch == '+';
return ch == '-' || ch == '+' || isNumber(ch);
}
function throwError(error, start, end) {
end = end || index;
throw Error("Lexer Error: " + error + " at column" +
(isDefined(start) ?
"s " + start + "-" + index + " [" + text.substring(start, end) + "]" :
" " + end) +
" in expression [" + text + "].");
}
function readNumber() {
var number = "";
var start = index;
while (index < text.length) {
var ch = text.charAt(index);
var ch = lowercase(text.charAt(index));
if (ch == '.' || isNumber(ch)) {
number += ch;
} else {
var peekCh = peek();
if (ch == 'E' && isExpOperator(peekCh)) {
if (ch == 'e' && isExpOperator(peekCh)) {
number += ch;
} else if (isExpOperator(ch) &&
peekCh && isNumber(peekCh) &&
number.charAt(number.length - 1) == 'E') {
number.charAt(number.length - 1) == 'e') {
number += ch;
} else if (isExpOperator(ch) &&
(!peekCh || !isNumber(peekCh)) &&
number.charAt(number.length - 1) == 'E') {
throw 'Lexer found invalid exponential value "' + text + '"';
number.charAt(number.length - 1) == 'e') {
throwError('Invalid exponent');
} else {
break;
}
@@ -151,7 +158,7 @@ function lex(text, parseStringsForObjects){
}
tokens.push({index:start, text:ident, fn:fn, json: OPERATORS[ident]});
}
function readString(quote) {
var start = index;
index++;
@@ -165,9 +172,7 @@ function lex(text, parseStringsForObjects){
if (ch == 'u') {
var hex = text.substring(index + 1, index + 5);
if (!hex.match(/[\da-f]{4}/i))
throw "Lexer Error: Invalid unicode escape [\\u" +
hex + "] starting at column '" +
start + "' in expression '" + text + "'.";
throwError( "Invalid unicode escape [\\u" + hex + "]");
index += 4;
string += String.fromCharCode(parseInt(hex, 16));
} else {
@@ -194,9 +199,7 @@ function lex(text, parseStringsForObjects){
}
index++;
}
throw "Lexer Error: Unterminated quote [" +
text.substring(start) + "] starting at column '" +
(start+1) + "' in expression '" + text + "'.";
throwError("Unterminated quote", start);
}
function readRegexp(quote) {
var start = index;
@@ -227,9 +230,7 @@ function lex(text, parseStringsForObjects){
}
index++;
}
throw "Lexer Error: Unterminated RegExp [" +
text.substring(start) + "] starting at column '" +
(start+1) + "' in expression '" + text + "'.";
throwError("Unterminated RegExp", start);
}
}
@@ -244,21 +245,21 @@ function parser(text, json){
statements: statements,
validator: validator,
filter: filter,
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
watch: watch
};
///////////////////////////////////
function error(msg, token) {
throw "Token '" + token.text +
"' is " + msg + " at column='" +
(token.index + 1) + "' of expression '" +
text + "' starting at '" + text.substring(token.index) + "'.";
function throwError(msg, token) {
throw Error("Parse Error: Token '" + token.text +
"' " + msg + " at column " +
(token.index + 1) + " of expression [" +
text + "] starting at [" + text.substring(token.index) + "].");
}
function peekToken() {
if (tokens.length === 0)
throw "Unexpected end of expression: " + text;
throw Error("Unexpected end of expression: " + text);
return tokens[0];
}
@@ -279,10 +280,7 @@ function parser(text, json){
if (token) {
if (json && !token.json) {
index = token.index;
throw "Expression at column='" +
token.index + "' of expression '" +
text + "' starting at '" + text.substring(token.index) +
"' is not valid json.";
throwError("is not valid json", token);
}
tokens.shift();
this.currentToken = token;
@@ -293,11 +291,7 @@ function parser(text, json){
function consume(e1){
if (!expect(e1)) {
var token = peek();
throw "Expecting '" + e1 + "' at column '" +
(token.index+1) + "' in '" +
text + "' got '" +
text.substring(token.index) + "'.";
throwError("is unexpected, expecting [" + e1 + "]", peek());
}
}
@@ -319,8 +313,7 @@ function parser(text, json){
function assertAllConsumed(){
if (tokens.length !== 0) {
throw "Did not understand '" + text.substring(tokens[0].index) +
"' while evaluating '" + text + "'.";
throwError("is extra token not part of expression", tokens[0]);
}
}
@@ -386,18 +379,7 @@ function parser(text, json){
}
function expression(){
return throwStmt();
}
function throwStmt(){
if (expect('throw')) {
var throwExp = assignment();
return function (self) {
throw throwExp(self);
};
} else {
return assignment();
}
return assignment();
}
function assignment(){
@@ -405,9 +387,8 @@ function parser(text, json){
var token;
if (token = expect('=')) {
if (!left.isAssignable) {
throw "Left hand side '" +
text.substring(0, token.index) + "' of assignment '" +
text.substring(token.index) + "' is not assignable.";
throwError("implies assignment but [" +
text.substring(0, token.index) + "] can not be assigned to", token);
}
var ident = function(){return left.isAssignable;};
return binaryFn(ident, token.fn, logicalOR());
@@ -497,8 +478,7 @@ function parser(text, json){
instance = instance[key];
}
if (typeof instance != $function) {
throw "Function '" + token.text + "' at column '" +
(token.index+1) + "' in '" + text + "' is not defined.";
throwError("should be a function", token);
}
return instance;
}
@@ -517,7 +497,7 @@ function parser(text, json){
var token = expect();
primary = token.fn;
if (!primary) {
error("not a primary expression", token);
throwError("not a primary expression", token);
}
}
var next;
@@ -529,7 +509,7 @@ function parser(text, json){
} else if (next.text === '.') {
primary = fieldAccess(primary);
} else {
throw "IMPOSSIBLE";
throwError("IMPOSSIBLE");
}
}
return primary;
@@ -624,6 +604,7 @@ function parser(text, json){
};
}
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
function watch () {
var decl = [];
while(hasTokens()) {
+290
View File
@@ -0,0 +1,290 @@
/*
* HTML Parser By Misko Hevery (misko@hevery.com)
* based on: HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
*/
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP = /^<\s*([\w:]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
END_TAG_REGEXP = /^<\s*\/\s*([\w:]+)[^>]*>/,
ATTR_REGEXP = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g;
// Empty Elements - HTML 4.01
var emptyElements = makeMap("area,base,basefont,br,col,hr,img,input,isindex,link,param");
// Block Elements - HTML 4.01
var blockElements = makeMap("address,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,"+
"form,hr,ins,isindex,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
// Inline Elements - HTML 4.01
var inlineElements = makeMap("a,abbr,acronym,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,img,"+
"input,ins,kbd,label,map,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelfElements = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
var validElements = extend({}, emptyElements, blockElements, inlineElements, closeSelfElements);
var validAttrs = extend({}, fillAttrs, makeMap(
'abbr,align,alink,alt,archive,axis,background,bgcolor,border,cellpadding,cellspacing,cite,class,classid,clear,code,codebase,'+
'codetype,color,cols,colspan,content,coords,data,dir,face,for,headers,height,href,hreflang,hspace,id,label,lang,language,'+
'link,longdesc,marginheight,marginwidth,maxlength,media,method,name,nowrap,profile,prompt,rel,rev,rows,rowspan,rules,scheme,'+
'scope,scrolling,shape,size,span,src,standby,start,summary,tabindex,target,text,title,type,usemap,valign,value,valuetype,'+
'vlink,vspace,width'));
/**
* @example
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
var htmlParser = function( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function(){ return stack[ stack.length - 1 ]; };
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") === 0 ) {
index = html.indexOf("-->");
if ( index >= 0 ) {
if ( handler.comment )
handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if ( handler.chars )
handler.chars( text );
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){
text = text.
replace(COMMENT_REGEXP, "$1").
replace(CDATA_REGEXP, "$1");
if ( handler.chars )
handler.chars( text );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last ) {
throw "Parse Error: " + html;
}
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
tagName = lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( closeSelfElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = emptyElements[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
if ( handler.start ) {
var attrs = {};
rest.replace(ATTR_REGEXP, function(match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs[name] = value; //value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
if ( handler.start )
handler.start( tagName, attrs, unary );
}
}
function parseEndTag( tag, tagName ) {
var pos = 0, i;
tagName = lowercase(tagName);
if ( tagName )
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if ( handler.end )
handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
};
/**
* @param str 'key1,key2,...'
* @returns {object} in the form of {key1:true, key2:true, ...}
*/
function makeMap(str){
var obj = {}, items = str.split(","), i;
for ( i = 0; i < items.length; i++ )
obj[ items[i] ] = true;
return obj;
}
/*
* For attack vectors see: http://ha.ckers.org/xss.html
*/
var JAVASCRIPT_URL = /^javascript:/i,
NBSP_REGEXP = /&nbsp;/gim,
HEX_ENTITY_REGEXP = /&#x([\da-f]*);?/igm,
DEC_ENTITY_REGEXP = /&#(\d+);?/igm,
CHAR_REGEXP = /[\w:]/gm,
HEX_DECODE = function(match, code){return fromCharCode(parseInt(code,16));},
DEC_DECODE = function(match, code){return fromCharCode(code);};
/**
* @param {string} url
* @returns true if url decodes to something which starts with 'javascript:' hence unsafe
*/
function isJavaScriptUrl(url) {
var chars = [];
url.replace(NBSP_REGEXP, '').
replace(HEX_ENTITY_REGEXP, HEX_DECODE).
replace(DEC_ENTITY_REGEXP, DEC_DECODE).
// Remove all non \w: characters, unfurtunetly value.replace(/[\w:]/,'') can be defeated using \u0000
replace(CHAR_REGEXP, function(ch){chars.push(ch);});
return JAVASCRIPT_URL.test(lowercase(chars.join('')));
}
/**
* create an HTML/XML writer which writes to buffer
* @param {Array} buf use buf.jain('') to get out sanitized html string
* @returns {object} in the form of {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf){
var ignore = false;
var out = bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
tag = lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
}
if (!ignore && validElements[tag]) {
out('<');
out(tag);
foreach(attrs, function(value, key){
if (validAttrs[lowercase(key)] && !isJavaScriptUrl(value)) {
out(' ');
out(key);
out('="');
out(value.
replace(/</g, '&lt;').
replace(/>/g, '&gt;').
replace(/\"/g,'&quot;'));
out('"');
}
});
out(unary ? '/>' : '>');
}
},
end: function(tag){
tag = lowercase(tag);
if (!ignore && validElements[tag]) {
out('</');
out(tag);
out('>');
}
if (tag == ignore) {
ignore = false;
}
},
chars: function(chars){
if (!ignore) {
out(chars.
replace(/&(\w+[&;\W])?/g, function(match, entity){return entity?match:'&amp;';}).
replace(/</g, '&lt;').
replace(/>/g, '&gt;'));
}
}
};
}
+96 -17
View File
@@ -1,51 +1,130 @@
/**
* Represents the application currently being tested and abstracts usage
* of iframes or separate windows.
*
* @param {Object} context jQuery wrapper around HTML context.
*/
angular.scenario.Application = function(context) {
this.context = context;
context.append('<h2>Current URL: <a href="about:blank">None</a></h2>');
context.append(
'<h2>Current URL: <a href="about:blank">None</a></h2>' +
'<div id="test-frames"></div>'
);
};
/**
* Gets the jQuery collection of frames. Don't use this directly because
* frames may go stale.
*
* @private
* @return {Object} jQuery collection
*/
angular.scenario.Application.prototype.getFrame = function() {
return this.context.find('> iframe');
angular.scenario.Application.prototype.getFrame_ = function() {
return this.context.find('#test-frames iframe:last');
};
/**
* Gets the window of the test runner frame. Always favor executeAction()
* Gets the window of the test runner frame. Always favor executeAction()
* instead of this method since it prevents you from getting a stale window.
*
* @private
* @return {Object} the window of the frame
*/
angular.scenario.Application.prototype.getWindow = function() {
var contentWindow = this.getFrame().attr('contentWindow');
angular.scenario.Application.prototype.getWindow_ = function() {
var contentWindow = this.getFrame_().attr('contentWindow');
if (!contentWindow)
throw 'No window available because frame not loaded.';
throw 'Frame window is not accessible.';
return contentWindow;
};
/**
* Changes the location of the frame.
* Checks that a URL would return a 2xx success status code. Callback is called
* with no arguments on success, or with an error on failure.
*
* Warning: This requires the server to be able to respond to HEAD requests
* and not modify the state of your application.
*
* @param {string} url Url to check
* @param {Function} callback function(error) that is called with result.
*/
angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
this.getFrame().remove();
this.context.append('<iframe src=""></iframe>');
this.context.find('> h2 a').attr('href', url).text(url);
this.getFrame().attr('src', url).load(onloadFn);
angular.scenario.Application.prototype.checkUrlStatus_ = function(url, callback) {
var self = this;
_jQuery.ajax({
url: url,
type: 'HEAD',
complete: function(request) {
if (request.status < 200 || request.status >= 300) {
if (!request.status) {
callback.call(self, 'Sandbox Error: Cannot access ' + url);
} else {
callback.call(self, request.status + ' ' + request.statusText);
}
} else {
callback.call(self);
}
}
});
};
/**
* Executes a function in the context of the tested application.
* Changes the location of the frame.
*
* @param {Function} The callback to execute. function($window, $document)
* @param {string} url The URL. If it begins with a # then only the
* hash of the page is changed.
* @param {Function} loadFn function($window, $document) Called when frame loads.
* @param {Function} errorFn function(error) Called if any error when loading.
*/
angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorFn) {
var self = this;
var frame = this.getFrame_();
//TODO(esprehn): Refactor to use rethrow()
errorFn = errorFn || function(e) { throw e; };
if (url === 'about:blank') {
errorFn('Sandbox Error: Navigating to about:blank is not allowed.');
} else if (url.charAt(0) === '#') {
url = frame.attr('src').split('#')[0] + url;
frame.attr('src', url);
this.executeAction(loadFn);
} else {
frame.css('display', 'none').attr('src', 'about:blank');
this.checkUrlStatus_(url, function(error) {
if (error) {
return errorFn(error);
}
self.context.find('#test-frames').append('<iframe>');
frame = this.getFrame_();
frame.load(function() {
frame.unbind();
try {
self.executeAction(loadFn);
} catch (e) {
errorFn(e);
}
}).attr('src', url);
});
}
this.context.find('> h2 a').attr('href', url).text(url);
};
/**
* Executes a function in the context of the tested application. Will wait
* for all pending angular xhr requests before executing.
*
* @param {Function} action The callback to execute. function($window, $document)
* $document is a jQuery wrapped document.
*/
angular.scenario.Application.prototype.executeAction = function(action) {
var $window = this.getWindow();
return action.call(this, $window, _jQuery($window.document));
var self = this;
var $window = this.getWindow_();
if (!$window.document) {
throw 'Sandbox Error: Application document not accessible.';
}
if (!$window.angular) {
return action.call(this, $window, _jQuery($window.document));
}
var $browser = $window.angular.service.$browser();
$browser.poll();
$browser.notifyWhenNoOutstandingRequests(function() {
action.call(self, $window, _jQuery($window.document));
});
};
+54 -11
View File
@@ -1,8 +1,12 @@
/**
* The representation of define blocks. Don't used directly, instead use
* define() in your tests.
*
* @param {string} descName Name of the block
* @param {Object} parent describe or undefined if the root.
*/
angular.scenario.Describe = function(descName, parent) {
this.only = parent && parent.only;
this.beforeEachFns = [];
this.afterEachFns = [];
this.its = [];
@@ -10,7 +14,7 @@ angular.scenario.Describe = function(descName, parent) {
this.name = descName;
this.parent = parent;
this.id = angular.scenario.Describe.id++;
/**
* Calls all before functions.
*/
@@ -36,7 +40,7 @@ angular.scenario.Describe.id = 0;
/**
* Defines a block to execute before each it or nested describe.
*
* @param {Function} Body of the block.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.beforeEach = function(body) {
this.beforeEachFns.push(body);
@@ -45,7 +49,7 @@ angular.scenario.Describe.prototype.beforeEach = function(body) {
/**
* Defines a block to execute after each it or nested describe.
*
* @param {Function} Body of the block.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.afterEach = function(body) {
this.afterEachFns.push(body);
@@ -54,8 +58,8 @@ angular.scenario.Describe.prototype.afterEach = function(body) {
/**
* Creates a new describe block that's a child of this one.
*
* @param {String} Name of the block. Appended to the parent block's name.
* @param {Function} Body of the block.
* @param {string} name Name of the block. Appended to the parent block's name.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.describe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
@@ -63,6 +67,19 @@ angular.scenario.Describe.prototype.describe = function(name, body) {
body.call(child);
};
/**
* Same as describe() but makes ddescribe blocks the only to run.
*
* @param {string} name Name of the test.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.ddescribe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
child.only = true;
this.children.push(child);
body.call(child);
};
/**
* Use to disable a describe block.
*/
@@ -71,20 +88,31 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop;
/**
* Defines a test.
*
* @param {String} Name of the test.
* @param {Function} Body of the block.
* @param {string} name Name of the test.
* @param {Function} vody Body of the block.
*/
angular.scenario.Describe.prototype.it = function(name, body) {
var self = this;
this.its.push({
definition: this,
only: this.only,
name: name,
before: self.setupBefore,
before: this.setupBefore,
body: body,
after: self.setupAfter
after: this.setupAfter
});
};
/**
* Same as it() but makes iit tests the only test to run.
*
* @param {string} name Name of the test.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.iit = function(name, body) {
this.it.apply(this, arguments);
this.its[this.its.length-1].only = true;
};
/**
* Use to disable a test block.
*/
@@ -93,6 +121,15 @@ angular.scenario.Describe.prototype.xit = angular.noop;
/**
* Gets an array of functions representing all the tests (recursively).
* that can be executed with SpecRunner's.
*
* @return {Array<Object>} Array of it blocks {
* definition : Object // parent Describe
* only: boolean
* name: string
* before: Function
* body: Function
* after: Function
* }
*/
angular.scenario.Describe.prototype.getSpecs = function() {
var specs = arguments[0] || [];
@@ -102,5 +139,11 @@ angular.scenario.Describe.prototype.getSpecs = function() {
angular.foreach(this.its, function(it) {
specs.push(it);
});
return specs;
var only = [];
angular.foreach(specs, function(it) {
if (it.only) {
only.push(it);
}
});
return (only.length && only) || specs;
};
+5 -3
View File
@@ -1,9 +1,9 @@
/**
* A future action in a spec.
*
* @param {String} name of the future action
* @param {string} name of the future action
* @param {Function} future callback(error, result)
* @param {String} Optional. function that returns the file/line number.
* @param {Function} Optional. function that returns the file/line number.
*/
angular.scenario.Future = function(name, behavior, line) {
this.name = name;
@@ -17,7 +17,7 @@ angular.scenario.Future = function(name, behavior, line) {
/**
* Executes the behavior of the closure.
*
* @param {Function} Callback function(error, result)
* @param {Function} doneFn Callback function(error, result)
*/
angular.scenario.Future.prototype.execute = function(doneFn) {
var self = this;
@@ -37,6 +37,8 @@ angular.scenario.Future.prototype.execute = function(doneFn) {
/**
* Configures the future to convert it's final with a function fn(value)
*
* @param {Function} fn function(value) that returns the parsed value
*/
angular.scenario.Future.prototype.parsedWith = function(fn) {
this.parser = fn;
-244
View File
@@ -1,244 +0,0 @@
/**
* User Interface for the Scenario Runner.
*
* @param {Object} The jQuery UI object for the UI.
*/
angular.scenario.ui.Html = function(context) {
this.context = context;
context.append(
'<div id="header">' +
' <h1><span class="angular">&lt;angular/&gt;</span>: Scenario Test Runner</h1>' +
' <ul id="status-legend" class="status-display">' +
' <li class="status-error">0 Errors</li>' +
' <li class="status-failure">0 Failures</li>' +
' <li class="status-success">0 Passed</li>' +
' </ul>' +
'</div>' +
'<div id="specs">' +
' <div class="test-children"></div>' +
'</div>'
);
};
/**
* The severity order of an error.
*/
angular.scenario.ui.Html.SEVERITY = ['pending', 'success', 'failure', 'error'];
/**
* Adds a new spec to the UI.
*
* @param {Object} The spec object created by the Describe object.
*/
angular.scenario.ui.Html.prototype.addSpec = function(spec) {
var self = this;
var specContext = this.findContext(spec.definition);
specContext.find('> .tests').append(
'<li class="status-pending test-it"></li>'
);
specContext = specContext.find('> .tests li:last');
return new angular.scenario.ui.Html.Spec(specContext, spec.name,
function(status) {
status = self.context.find('#status-legend .status-' + status);
var parts = status.text().split(' ');
var value = (parts[0] * 1) + 1;
status.text(value + ' ' + parts[1]);
}
);
};
/**
* Finds the context of a spec block defined by the passed definition.
*
* @param {Object} The definition created by the Describe object.
*/
angular.scenario.ui.Html.prototype.findContext = function(definition) {
var self = this;
var path = [];
var currentContext = this.context.find('#specs');
var currentDefinition = definition;
while (currentDefinition && currentDefinition.name) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
angular.foreach(path, function(defn) {
var id = 'describe-' + defn.id;
if (!self.context.find('#' + id).length) {
currentContext.find('> .test-children').append(
'<div class="test-describe" id="' + id + '">' +
' <h2></h2>' +
' <div class="test-children"></div>' +
' <ul class="tests"></ul>' +
'</div>'
);
self.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
}
currentContext = self.context.find('#' + id);
});
return this.context.find('#describe-' + definition.id);
};
/**
* A spec block in the UI.
*
* @param {Object} The jQuery object for the context of the spec.
* @param {String} The name of the spec.
* @param {Function} Callback function(status) to call when complete.
*/
angular.scenario.ui.Html.Spec = function(context, name, doneFn) {
this.status = 'pending';
this.context = context;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
'<div class="test-info">' +
' <p class="test-title">' +
' <span class="timer-result"></span>' +
' <span class="test-name"></span>' +
' </p>' +
'</div>' +
'<div class="scrollpane">' +
' <ol class="test-actions">' +
' </ol>' +
'</div>'
);
context.find('> .test-info').click(function() {
var scrollpane = context.find('> .scrollpane');
var actions = scrollpane.find('> .test-actions');
var name = context.find('> .test-info .test-name');
if (actions.find(':visible').length) {
actions.hide();
name.removeClass('open').addClass('closed');
} else {
actions.show();
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
name.removeClass('closed').addClass('open');
}
});
context.find('> .test-info .test-name').text('it ' + name);
};
/**
* Adds a new Step to this spec and returns it.
*
* @param {String} The name of the step.
* @param {Function} function() that returns a string with the file/line number
* where the step was added from.
*/
angular.scenario.ui.Html.Spec.prototype.addStep = function(name, location) {
this.context.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
var stepContext = this.context.find('> .scrollpane .test-actions li:last');
var self = this;
return new angular.scenario.ui.Html.Step(stepContext, name, location, function(status) {
if (indexOf(angular.scenario.ui.Html.SEVERITY, status) >
indexOf(angular.scenario.ui.Html.SEVERITY, self.status)) {
self.status = status;
}
var scrollpane = self.context.find('> .scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
});
};
/**
* Completes the spec and sets the timer value.
*/
angular.scenario.ui.Html.Spec.prototype.complete = function() {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find("> .test-info .timer-result").
text((endTime - this.startTime) + "ms");
if (this.status === 'success') {
this.context.find('> .test-info .test-name').addClass('closed');
this.context.find('> .scrollpane .test-actions').hide();
}
};
/**
* Finishes the spec, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Spec.prototype.finish = function() {
this.complete();
this.context.addClass('status-' + this.status);
this.doneFn(this.status);
};
/**
* Finishes the spec, but with a Fatal Error.
*
* @param {Object} Required error
*/
angular.scenario.ui.Html.Spec.prototype.error = function(error) {
this.status = 'error';
this.context.append('<pre></pre>');
this.context.find('> pre').text(formatException(error));
this.finish();
};
/**
* A single step inside an it block (or a before/after function).
*
* @param {Object} The jQuery object for the context of the step.
* @param {String} The name of the step.
* @param {Function} function() that returns file/line number of step.
* @param {Function} Callback function(status) to call when complete.
*/
angular.scenario.ui.Html.Step = function(context, name, location, doneFn) {
this.context = context;
this.name = name;
this.location = location;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
'<div class="timer-result"></div>' +
'<div class="test-title"></div>'
);
context.find('> .test-title').text(name);
var scrollpane = context.parents('.scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
};
/**
* Completes the step and sets the timer value.
*/
angular.scenario.ui.Html.Step.prototype.complete = function(error) {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find(".timer-result").
text((endTime - this.startTime) + "ms");
if (error) {
if (!this.context.find('.test-title pre').length) {
this.context.find('.test-title').append('<pre></pre>');
}
var message = _jQuery.trim(this.location() + '\n\n' + formatException(error));
this.context.find('.test-title pre').text(message);
}
};
/**
* Finishes the step, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Step.prototype.finish = function(error) {
this.complete(error);
if (error) {
this.context.addClass('status-failure');
this.doneFn('failure');
} else {
this.context.addClass('status-success');
this.doneFn('success');
}
};
/**
* Finishes the step, but with a Fatal Error.
*
* @param {Object} Required error
*/
angular.scenario.ui.Html.Step.prototype.error = function(error) {
this.complete(error);
this.context.addClass('status-error');
this.doneFn('error');
};
+153
View File
@@ -0,0 +1,153 @@
/**
* Maintains an object tree from the runner events.
*
* @param {Object} runner The scenario Runner instance to connect to.
*
* TODO(esprehn): Every output type creates one of these, but we probably
* want one glonal shared instance. Need to handle events better too
* so the HTML output doesn't need to do spec model.getSpec(spec.id)
* silliness.
*/
angular.scenario.ObjectModel = function(runner) {
var self = this;
this.specMap = {};
this.value = {
name: '',
children: {}
};
runner.on('SpecBegin', function(spec) {
var block = self.value;
angular.foreach(self.getDefinitionPath(spec), function(def) {
if (!block.children[def.name]) {
block.children[def.name] = {
id: def.id,
name: def.name,
children: {},
specs: {}
};
}
block = block.children[def.name];
});
self.specMap[spec.id] = block.specs[spec.name] =
new angular.scenario.ObjectModel.Spec(spec.id, spec.name);
});
runner.on('SpecError', function(spec, error) {
var it = self.getSpec(spec.id);
it.status = 'error';
it.error = error;
});
runner.on('SpecEnd', function(spec) {
var it = self.getSpec(spec.id);
complete(it);
});
runner.on('StepBegin', function(spec, step) {
var it = self.getSpec(spec.id);
it.steps.push(new angular.scenario.ObjectModel.Step(step.name));
});
runner.on('StepEnd', function(spec, step) {
var it = self.getSpec(spec.id);
if (it.getLastStep().name !== step.name)
throw 'Events fired in the wrong order. Step names don\' match.';
complete(it.getLastStep());
});
runner.on('StepFailure', function(spec, step, error) {
var it = self.getSpec(spec.id);
var item = it.getLastStep();
item.error = error;
if (!it.status) {
it.status = item.status = 'failure';
}
});
runner.on('StepError', function(spec, step, error) {
var it = self.getSpec(spec.id);
var item = it.getLastStep();
it.status = 'error';
item.status = 'error';
item.error = error;
});
function complete(item) {
item.endTime = new Date().getTime();
item.duration = item.endTime - item.startTime;
item.status = item.status || 'success';
}
};
/**
* Computes the path of definition describe blocks that wrap around
* this spec.
*
* @param spec Spec to compute the path for.
* @return {Array<Describe>} The describe block path
*/
angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) {
var path = [];
var currentDefinition = spec.definition;
while (currentDefinition && currentDefinition.name) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
return path;
};
/**
* Gets a spec by id.
*
* @param {string} The id of the spec to get the object for.
* @return {Object} the Spec instance
*/
angular.scenario.ObjectModel.prototype.getSpec = function(id) {
return this.specMap[id];
};
/**
* A single it block.
*
* @param {string} id Id of the spec
* @param {string} name Name of the spec
*/
angular.scenario.ObjectModel.Spec = function(id, name) {
this.id = id;
this.name = name;
this.startTime = new Date().getTime();
this.steps = [];
};
/**
* Adds a new step to the Spec.
*
* @param {string} step Name of the step (really name of the future)
* @return {Object} the added step
*/
angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) {
var step = new angular.scenario.ObjectModel.Step(name);
this.steps.push(step);
return step;
};
/**
* Gets the most recent step.
*
* @return {Object} the step
*/
angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {
return this.steps[this.steps.length-1];
};
/**
* A single step inside a Spec.
*
* @param {string} step Name of the step
*/
angular.scenario.ObjectModel.Step = function(name) {
this.name = name;
this.startTime = new Date().getTime();
};
+107 -21
View File
@@ -2,13 +2,16 @@
* Runner for scenarios.
*/
angular.scenario.Runner = function($window) {
this.listeners = [];
this.$window = $window;
this.rootDescribe = new angular.scenario.Describe();
this.currentDescribe = this.rootDescribe;
this.api = {
it: this.it,
iit: this.iit,
xit: angular.noop,
describe: this.describe,
ddescribe: this.ddescribe,
xdescribe: angular.noop,
beforeEach: this.beforeEach,
afterEach: this.afterEach
@@ -18,11 +21,42 @@ angular.scenario.Runner = function($window) {
}));
};
/**
* Emits an event which notifies listeners and passes extra
* arguments.
*
* @param {string} eventName Name of the event to fire.
*/
angular.scenario.Runner.prototype.emit = function(eventName) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
eventName = eventName.toLowerCase();
if (!this.listeners[eventName])
return;
angular.foreach(this.listeners[eventName], function(listener) {
listener.apply(self, args);
});
};
/**
* Adds a listener for an event.
*
* @param {string} eventName The name of the event to add a handler for
* @param {string} listener The fn(...) that takes the extra arguments from emit()
*/
angular.scenario.Runner.prototype.on = function(eventName, listener) {
eventName = eventName.toLowerCase();
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(listener);
};
/**
* Defines a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.describe = function(name, body) {
var self = this;
@@ -37,20 +71,57 @@ angular.scenario.Runner.prototype.describe = function(name, body) {
});
};
/**
* Same as describe, but makes ddescribe the only blocks to run.
*
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.ddescribe = function(name, body) {
var self = this;
this.currentDescribe.ddescribe(name, function() {
var parentDescribe = self.currentDescribe;
self.currentDescribe = this;
try {
body.call(this);
} finally {
self.currentDescribe = parentDescribe;
}
});
};
/**
* Defines a test in a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.it = function(name, body) {
this.currentDescribe.it(name, body);
};
/**
* Same as it, but makes iit tests the only tests to run.
*
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.iit = function(name, body) {
this.currentDescribe.iit(name, body);
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
*
* @see Describe.js
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.beforeEach = function(body) {
@@ -61,6 +132,8 @@ angular.scenario.Runner.prototype.beforeEach = function(body) {
* Defines a function to be called after each it block in the describe
* (and before all nested describes).
*
* @see Describe.js
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.afterEach = function(body) {
@@ -68,24 +141,29 @@ angular.scenario.Runner.prototype.afterEach = function(body) {
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
* Creates a new spec runner.
*
* @param {Function} Callback to execute
* @private
* @param {Object} scope parent scope
*/
angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
var $root = angular.scope({}, angular.service);
angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
return scope.$new(angular.scenario.SpecRunner);
};
/**
* Runs all the loaded tests with the specified runner class on the
* provided application.
*
* @param {angular.scenario.Application} application App to remote control.
*/
angular.scenario.Runner.prototype.run = function(application) {
var self = this;
var specs = this.rootDescribe.getSpecs();
var $root = angular.scope(this);
$root.application = application;
$root.ui = ui;
$root.setTimeout = function() {
return self.$window.setTimeout.apply(self.$window, arguments);
};
asyncForEach(specs, function(spec, specDone) {
this.emit('RunnerBegin');
asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
var dslCache = {};
var runner = angular.scope($root);
runner.$become(specRunnerClass);
var runner = self.createSpecRunner_($root);
angular.foreach(angular.scenario.dsl, function(fn, key) {
dslCache[key] = fn.call($root);
});
@@ -105,16 +183,24 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
// Make these methods work on the current chain
scope.addFuture = function() {
Array.prototype.push.call(arguments, line);
return specRunnerClass.prototype.addFuture.apply(scope, arguments);
return angular.scenario.SpecRunner.
prototype.addFuture.apply(scope, arguments);
};
scope.addFutureAction = function() {
Array.prototype.push.call(arguments, line);
return specRunnerClass.prototype.addFutureAction.apply(scope, arguments);
return angular.scenario.SpecRunner.
prototype.addFutureAction.apply(scope, arguments);
};
return scope.dsl[key].apply(scope, arguments);
};
});
runner.run(ui, spec, specDone);
}, specsDone || angular.noop);
runner.run(spec, specDone);
},
function(error) {
if (error) {
self.emit('RunnerError', error);
}
self.emit('RunnerEnd');
});
};
+135 -23
View File
@@ -1,3 +1,4 @@
/**
* Setup file for the Scenario.
* Must be first in the compilation/bootstrap list.
@@ -6,8 +7,15 @@
// Public namespace
angular.scenario = angular.scenario || {};
// Namespace for the UI
angular.scenario.ui = angular.scenario.ui || {};
/**
* Defines a new output format.
*
* @param {string} name the name of the new output format
* @param {Function} fn function(context, runner) that generates the output
*/
angular.scenario.output = angular.scenario.output || function(name, fn) {
angular.scenario.output[name] = fn;
};
/**
* Defines a new DSL statement. If your factory function returns a Future
@@ -18,8 +26,8 @@ angular.scenario.ui = angular.scenario.ui || {};
* set on "this" in your statement function are available in the chained
* functions.
*
* @param {String} The name of the statement
* @param {Function} Factory function(application), return a function for
* @param {string} name The name of the statement
* @param {Function} fn Factory function(), return a function for
* the statement.
*/
angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
@@ -54,8 +62,8 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
* against. Your function should return a boolean. The future is automatically
* created for you.
*
* @param {String} The name of the matcher
* @param {Function} The matching function(expected).
* @param {string} name The name of the matcher
* @param {Function} fn The matching function(expected).
*/
angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
angular.scenario.matcher[name] = function(expected) {
@@ -64,7 +72,7 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
prefix += 'not ';
}
var self = this;
this.addFuture(prefix + name + ' ' + angular.toJson(expected),
this.addFuture(prefix + name + ' ' + angular.toJson(expected),
function(done) {
var error;
self.actual = self.future.value;
@@ -78,14 +86,66 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
};
};
/**
* Initialization function for the scenario runner.
*
* @param {angular.scenario.Runner} $scenario The runner to setup
* @param {Object} config Config options
*/
function angularScenarioInit($scenario, config) {
var href = window.location.href;
var body = _jQuery(document.body);
var output = [];
if (config.scenario_output) {
output = config.scenario_output.split(',');
}
angular.foreach(angular.scenario.output, function(fn, name) {
if (!output.length || indexOf(output,name) != -1) {
var context = body.append('<div></div>').find('div:last');
context.attr('id', name);
fn.call({}, context, $scenario);
}
});
if (!/^http/.test(href) && !/^https/.test(href)) {
body.append('<p id="system-error"></p>');
body.find('#system-error').text(
'Scenario runner must be run using http or https. The protocol ' +
href.split(':')[0] + ':// is not supported.'
);
return;
}
var appFrame = body.append('<div id="application"></div>').find('#application');
var application = new angular.scenario.Application(appFrame);
$scenario.on('RunnerEnd', function() {
appFrame.css('display', 'none');
appFrame.find('iframe').attr('src', 'about:blank');
});
$scenario.on('RunnerError', function(error) {
if (window.console) {
console.log(formatException(error));
} else {
// Do something for IE
alert(error);
}
});
$scenario.run(application);
}
/**
* Iterates through list with iterator function that must call the
* continueFunction to continute iterating.
*
* @param {Array} list to iterate over
* @param {Function} Callback function(value, continueFunction)
* @param {Function} Callback function(error, result) called when iteration
* finishes or an error occurs.
* @param {Array} list list to iterate over
* @param {Function} iterator Callback function(value, continueFunction)
* @param {Function} done Callback function(error, result) called when
* iteration finishes or an error occurs.
*/
function asyncForEach(list, iterator, done) {
var i = 0;
@@ -110,8 +170,8 @@ function asyncForEach(list, iterator, done) {
* Formats an exception into a string with the stack trace, but limits
* to a specific line length.
*
* @param {Object} the exception to format, can be anything throwable
* @param {Number} Optional. max lines of the stack trace to include
* @param {Object} error The exception to format, can be anything throwable
* @param {Number} maxStackLines Optional. max lines of the stack trace to include
* default is 5.
*/
function formatException(error, maxStackLines) {
@@ -129,18 +189,20 @@ function formatException(error, maxStackLines) {
}
/**
* Returns a function that gets the file name and line number from a
* Returns a function that gets the file name and line number from a
* location in the stack if available based on the call site.
*
* Note: this returns another function because accessing .stack is very
* expensive in Chrome.
*
* @param {Number} offset Number of stack lines to skip
*/
function callerFile(offset) {
var error = new Error();
return function() {
var line = (error.stack || '').split('\n')[offset];
// Clean up the stack trace line
if (line) {
if (line.indexOf('@') !== -1) {
@@ -151,7 +213,7 @@ function callerFile(offset) {
line = line.substring(line.indexOf('(')+1).replace(')', '');
}
}
return line || '';
};
}
@@ -161,7 +223,7 @@ function callerFile(offset) {
* not specified.
*
* @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement
* @param {String} Optional event type.
* @param {string} Optional event type.
*/
function browserTrigger(element, type) {
if (element && !element.nodeName) element = element[0];
@@ -188,7 +250,22 @@ function browserTrigger(element, type) {
type = 'change';
}
if (msie) {
switch(element.type) {
case 'radio':
case 'checkbox':
element.checked = !element.checked;
break;
}
element.fireEvent('on' + type);
if (lowercase(element.type) == 'submit') {
while(element) {
if (lowercase(element.nodeName) == 'form') {
element.fireEvent('onsubmit');
break;
}
element = element.parentNode;
}
}
} else {
var evnt = document.createEvent('MouseEvents');
evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
@@ -200,13 +277,48 @@ function browserTrigger(element, type) {
* Don't use the jQuery trigger method since it works incorrectly.
*
* jQuery notifies listeners and then changes the state of a checkbox and
* does not create a real browser event. A real click changes the state of
* does not create a real browser event. A real click changes the state of
* the checkbox and then notifies listeners.
*
*
* To work around this we instead use our own handler that fires a real event.
*/
_jQuery.fn.trigger = function(type) {
return this.each(function(index, node) {
browserTrigger(node, type);
(function(fn){
var parentTrigger = fn.trigger;
fn.trigger = function(type) {
if (/(click|change|keyup)/.test(type)) {
return this.each(function(index, node) {
browserTrigger(node, type);
});
}
return parentTrigger.apply(this, arguments);
};
})(_jQuery.fn);
/**
* Finds all bindings with the substring match of name and returns an
* array of their values.
*
* @param {string} name The name to match
* @return {Array.<string>} String of binding values
*/
_jQuery.fn.bindings = function(name) {
function contains(text, value) {
return value instanceof RegExp ?
value.test(text) :
text && text.indexOf(value) >= 0;
}
var result = [];
this.find('.ng-binding:visible').each(function() {
var element = new _jQuery(this);
if (!angular.isDefined(name) ||
contains(element.attr('ng:bind'), name) ||
contains(element.attr('ng:bind-template'), name)) {
if (element.is('input, textarea')) {
result.push(element.val());
} else {
result.push(element.html());
}
}
});
return result;
};
+32 -25
View File
@@ -15,14 +15,15 @@ angular.scenario.SpecRunner = function() {
* Executes a spec which is an it block with associated before/after functions
* based on the describe nesting.
*
* @param {Object} An angular.scenario.UI implementation
* @param {Object} A spec object
* @param {Object} An angular.scenario.Application instance
* @param {Object} spec A spec object
* @param {Object} specDone An angular.scenario.Application instance
* @param {Function} Callback function that is called when the spec finshes.
*/
angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {
var self = this;
var specUI = ui.addSpec(spec);
this.spec = spec;
this.emit('SpecBegin', spec);
try {
spec.before.call(this);
@@ -30,7 +31,8 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
this.afterIndex = this.futures.length;
spec.after.call(this);
} catch (e) {
specUI.error(e);
this.emit('SpecError', spec, e);
this.emit('SpecEnd', spec);
specDone();
return;
}
@@ -42,32 +44,36 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
self.error = true;
done(null, self.afterIndex);
};
var spec = this;
asyncForEach(
this.futures,
function(future, futureDone) {
var stepUI = specUI.addStep(future.name, future.line);
self.step = future;
self.emit('StepBegin', spec, future);
try {
future.execute(function(error) {
stepUI.finish(error);
if (error) {
self.emit('StepFailure', spec, future, error);
self.emit('StepEnd', spec, future);
return handleError(error, futureDone);
}
spec.$window.setTimeout( function() { futureDone(); }, 0);
self.emit('StepEnd', spec, future);
self.$window.setTimeout(function() { futureDone(); }, 0);
});
} catch (e) {
stepUI.error(e);
self.emit('StepError', spec, future, e);
self.emit('StepEnd', spec, future);
handleError(e, futureDone);
}
},
function(e) {
if (e) {
specUI.error(e);
} else {
specUI.finish();
self.emit('SpecError', spec, e);
}
specDone();
self.emit('SpecEnd', spec);
// Call done in a timeout so exceptions don't recursively
// call this function
self.$window.setTimeout(function() { specDone(); }, 0);
}
);
};
@@ -77,9 +83,9 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
*
* Note: Do not pass line manually. It happens automatically.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
* @param {Function} fn() that returns file/line number
* @param {string} name Name of the future
* @param {Function} behavior Behavior of the future
* @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
@@ -92,19 +98,20 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line)
*
* Note: Do not pass line manually. It happens automatically.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
* @param {Function} fn() that returns file/line number
* @param {string} name Name of the future
* @param {Function} behavior Behavior of the future
* @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
var self = this;
return this.addFuture(name, function(done) {
this.application.executeAction(function($window, $document) {
//TODO(esprehn): Refactor this so it doesn't need to be in here.
$document.elements = function(selector) {
var args = Array.prototype.slice.call(arguments, 1);
if (self.selector) {
selector = self.selector + ' ' + (selector || '');
}
selector = (self.selector || '') + ' ' + (selector || '');
selector = _jQuery.trim(selector) || '*';
angular.foreach(args, function(value, index) {
selector = selector.replace('$' + (index + 1), value);
});
@@ -1,6 +1,6 @@
(function(previousOnLoad){
var prefix = (function(){
var filename = /(.*\/)bootstrap.js(#(.*))?/;
var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
for(var j = 0; j < scripts.length; j++) {
var src = scripts[j].src;
@@ -23,50 +23,36 @@
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
_jQuery(document.body).append(
'<div id="runner"></div>' +
'<div id="frame"></div>'
);
var frame = _jQuery('#frame');
var runner = _jQuery('#runner');
var application = new angular.scenario.Application(frame);
var ui = new angular.scenario.ui.Html(runner);
$scenario.run(ui, application, angular.scenario.SpecRunner, function(error) {
frame.remove();
if (error) {
if (window.console) {
console.log(error.stack || error);
} else {
// Do something for IE
alert(error);
}
}
});
angularScenarioInit($scenario, angularJsConfig(document));
};
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery-1.4.2.js");
document.write(
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'</script>'
);
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'</script>'
);
addScript("../angular-bootstrap.js");
addScript("Scenario.js");
addScript("Application.js");
addScript("Describe.js");
addScript("Future.js");
addScript("HtmlUI.js");
addScript("Runner.js");
addScript("SpecRunner.js");
addScript("dsl.js");
addScript("matchers.js");
addScript("ObjectModel.js");
addScript("output/Html.js");
addScript("output/Json.js");
addScript("output/Object.js");
addScript("output/Xml.js");
// Create the runner (which also sets up the global API)
document.write(
'<script type="text/javascript">' +
'var $scenario = new angular.scenario.Runner(window);' +
'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' +
'</script>'
);
+1 -1
View File
@@ -22,4 +22,4 @@
* THE SOFTWARE.
*/
(function(window, document, previousOnLoad){
var _jQuery = window.jQuery.noConflict(true);
var _jQuery = window.jQuery.noConflict(true);

Some files were not shown because too many files have changed in this diff Show More