├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── README.md
├── bower.json
├── dist
├── angular-pickadate.css
├── angular-pickadate.js
└── angular-pickadate.min.js
├── gulpfile.js
├── karma.conf.js
├── package.json
├── src
├── angular-pickadate.js
└── angular-pickadate.scss
└── test
├── angular-pickadate.spec.js
├── lib
├── angular-1.2.21.js
├── angular-1.3.6.js
├── angular-1.4.0.js
├── angular-mocks-1.2.21.js
├── angular-mocks-1.3.6.js
├── angular-mocks-1.4.0.js
└── browser_trigger.js
└── pickadate-date_helper.spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | // JSHint Default Configuration File (as on JSHint website)
3 | // See http://jshint.com/docs/ for more details
4 |
5 | "maxerr" : 50, // {int} Maximum error before stopping
6 |
7 | // Enforcing
8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
9 | "camelcase" : true, // true: Identifiers must be in camelCase
10 | "curly" : false, // true: Require {} for every new block or scope
11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison
12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
13 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
14 | "indent" : 2, // {int} Number of spaces to use for indentation
15 | "latedef" : false, // true: Require variables/functions to be defined before being used
16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
18 | "noempty" : true, // true: Prohibit use of empty blocks
19 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
20 | "plusplus" : false, // true: Prohibit use of `++` & `--`
21 | "quotmark" : false, // Quotation mark consistency:
22 | // false : do nothing (default)
23 | // true : ensure whatever is used is consistent
24 | // "single" : require single quotes
25 | // "double" : require double quotes
26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
27 | "unused" : true, // true: Require all defined variables be used
28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode
29 | "maxparams" : false, // {int} Max number of formal params allowed per function
30 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
31 | "maxstatements" : false, // {int} Max number statements per function
32 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function
33 | "maxlen" : false, // {int} Max number of characters per line
34 |
35 | // Relaxing
36 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected
38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
39 | "eqnull" : false, // true: Tolerate use of `== null`
40 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
41 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
42 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
43 | // (ex: `for each`, multiple try/catch, function expression…)
44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()`
45 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs
46 | "funcscope" : false, // true: Tolerate defining variables inside control statements
47 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
48 | "iterator" : false, // true: Tolerate using the `__iterator__` property
49 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
50 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings
51 | "laxcomma" : false, // true: Tolerate comma-first style coding
52 | "loopfunc" : false, // true: Tolerate functions being defined in loops
53 | "multistr" : false, // true: Tolerate multi-line strings
54 | "proto" : false, // true: Tolerate using the `__proto__` property
55 | "scripturl" : false, // true: Tolerate script-targeted URLs
56 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
57 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
58 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
59 | "validthis" : false, // true: Tolerate using this in a non-constructor function
60 |
61 | // Environments
62 | "browser" : true, // Web Browser (window, document, etc)
63 | "browserify" : false, // Browserify (node.js code in the browser)
64 | "couch" : false, // CouchDB
65 | "devel" : true, // Development/debugging (alert, confirm, etc)
66 | "dojo" : false, // Dojo Toolkit
67 | "jquery" : true, // jQuery
68 | "mootools" : false, // MooTools
69 | "node" : false, // Node.js
70 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
71 | "prototypejs" : false, // Prototype and Scriptaculous
72 | "rhino" : false, // Rhino
73 | "worker" : false, // Web Workers
74 | "wsh" : false, // Windows Scripting Host
75 | "yui" : false, // Yahoo User Interface
76 |
77 | // Custom Globals
78 | "globals" : {
79 | "describe" : true,
80 | "beforeEach" : true,
81 | "afterEach" : true,
82 | "inject" : true,
83 | "it" : true,
84 | "angular" : true,
85 | "module" : true,
86 | "expect" : true,
87 | "browserTrigger" : true,
88 | "sinon" : true
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.10'
4 |
5 | script:
6 | - npm test
7 |
8 | sudo: false
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Restorando
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular-pickadate [](https://travis-ci.org/restorando/angular-pickadate)
2 |
3 |
4 | A simple and fluid inline datepicker for AngularJS with no extra dependencies.
5 |
6 | 
7 |
8 | ### Demo
9 |
10 | View demo in a new window
11 |
12 | ### Installation
13 |
14 | 1) Add the `pickadate` module to your dependencies
15 |
16 | ```javascript
17 | angular.module('myApp', ['pickadate']);
18 | ```
19 |
20 | 2) Use the `pickadate` directive in any element
21 |
22 | ```html
23 |
24 | ```
25 |
26 | If the element is an ``, it will display the datepicker as a modal. Otherwise, it will be rendered inline.
27 |
28 | Pickadate is fluid, so it will take the width of the parent container.
29 |
30 | ### Pickadate options
31 |
32 | #### format
33 |
34 | You can specify the date format using the `format` attribute. Supported formats must have the year, month and day parts, and the separator must be `-` or `/`.
35 |
36 | ```html
37 |
38 | ```
39 |
40 | Format string can be composed of the following elements:
41 |
42 | * `'yyyy;`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010)
43 | * `'mm'` or `'MM'`: Month in year, padded (01-12)
44 | * `'dd'`: Day in month, padded (01-31)
45 |
46 | Every option that receives a date as the input (e.g. min-date, max-date, etc) should be entered using the same format.
47 |
48 | #### min-date, max-date
49 |
50 | ```html
51 |
52 | ```
53 |
54 | ```javascript
55 | function MyAppController($scope) {
56 | $scope.minDate = '2013-11-10';
57 | $scope.maxDate = '2013-12-31';
58 | }
59 | ```
60 |
61 | `min-date` and `max-date` take angular expressions, so if you want to specify the values inline, don't forget the quotes!
62 |
63 | ```html
64 |
65 | ```
66 |
67 | #### disabled-dates
68 |
69 | You can specify a function that will determine if a date is disabled or not. You can pass `formattedDate` as an argument to receive the date formatted in the current locale, or `date` to receive the date object. You can pass both of them if you need.
70 |
71 | ```html
72 |
73 | ```
74 |
75 | ```javascript
76 | function MyAppController($scope) {
77 | $scope.disabledDatesFn = function(formattedDate) {
78 | return ['2013-11-10', '2013-11-15', '2013-11-19'].indexOf(formattedDate) > -1;
79 | }
80 | }
81 | ```
82 |
83 | This is handy if you want to disable dates programatically.
84 |
85 | ```html
86 |
87 | ```
88 |
89 | ```javascript
90 | function MyAppController($scope) {
91 | $scope.disabledDatesFn = function(date) {
92 | return date.getDay() === 6; // Disable every Sunday
93 | }
94 | }
95 | ```
96 |
97 | #### default-date
98 |
99 | Allows you to preset the calendar to a particular month without setting the chosen date.
100 |
101 | ```html
102 |
103 | ```
104 |
105 | ```javascript
106 | function MyAppController($scope) {
107 | $scope.presetDate = '2013-12-01';
108 | }
109 | ```
110 |
111 | #### week-starts-on
112 |
113 | Sets the first day of the week. The default is 0 for Sunday.
114 |
115 | ```html
116 |
117 | ```
118 |
119 | #### no-extra-rows
120 |
121 | The calendar will have between 4 and 6 rows if this attribute is present. By default it will always have 6 rows.
122 |
123 | ```html
124 |
125 | ```
126 |
127 | #### multiple
128 |
129 | The calendar will support selecting multiple dates. NgModel will be set as an array of date strings
130 |
131 | ```html
132 |
133 | ```
134 |
135 | #### allow-blank-date
136 |
137 | The calendar will allow user to remove the last remaining date.
138 |
139 | ```html
140 |
141 | ```
142 |
143 | #### select-other-months
144 |
145 | This attribute can take the following string values:
146 |
147 | - `next`: the calendar will allow selecting dates from the next month
148 | - `previous`: same as the one before, but for previous month dates
149 | - `both`: the calendar will allow selecting dates from both the next and previous month
150 |
151 | ```html
152 |
153 | ```
154 |
155 | ### I18n & Icons
156 |
157 | Pickadate uses angular `$locale` module for the date translations. If you want to have the calendar in any other language, please include the corresponding AngularJS i18n files. You can get them here: [https://code.angularjs.org/1.3.0/i18n/](https://code.angularjs.org/1.3.0/i18n/).
158 |
159 | For the remaining translations you can configure the `pickadateI18nProvider`.
160 |
161 | ```javascript
162 | angular.module('testApp', ['pickadate'])
163 |
164 | .config(function(pickadateI18nProvider) {
165 | pickadateI18nProvider.translations = {
166 | prev: ' ant',
167 | next: 'sig '
168 | }
169 | });
170 | ```
171 |
172 | The translations can contain custom html code, useful to include custom icons in the calendar controls.
173 |
174 | ## License
175 |
176 | Copyright (c) 2013 Restorando
177 |
178 | MIT License
179 |
180 | Permission is hereby granted, free of charge, to any person obtaining
181 | a copy of this software and associated documentation files (the
182 | "Software"), to deal in the Software without restriction, including
183 | without limitation the rights to use, copy, modify, merge, publish,
184 | distribute, sublicense, and/or sell copies of the Software, and to
185 | permit persons to whom the Software is furnished to do so, subject to
186 | the following conditions:
187 |
188 | The above copyright notice and this permission notice shall be
189 | included in all copies or substantial portions of the Software.
190 |
191 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
192 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
193 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
194 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
195 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
196 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
197 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
198 |
199 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-pickadate",
3 | "version": "2.0.2",
4 | "homepage": "https://github.com/restorando/angular-pickadate",
5 | "authors": [
6 | "Gabriel Schammah "
7 | ],
8 | "description": "A simple and fluid inline datepicker for AngularJS with no extra dependencies",
9 | "keywords": [
10 | "datepicker",
11 | "angular",
12 | "angularjs",
13 | "pickadate",
14 | "date"
15 | ],
16 | "main": [
17 | "dist/angular-pickadate.js",
18 | "dist/angular-pickadate.css"
19 | ],
20 | "dependencies": {
21 | "angular": "1.x"
22 | },
23 | "license": "MIT",
24 | "ignore": [
25 | "**/.*",
26 | "node_modules",
27 | "bower_components",
28 | "test",
29 | "example",
30 | "package.json",
31 | "gulpfile.js",
32 | "karma.conf.js",
33 | "LICENSE"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/dist/angular-pickadate.css:
--------------------------------------------------------------------------------
1 | .pickadate {
2 | font-family: 'Helvetica Neue', Helvetica, Helvetica, Arial, sans-serif; }
3 | .pickadate a {
4 | color: #666666; }
5 | .pickadate a:visited {
6 | color: #666666; }
7 |
8 | .pickadate-header {
9 | position: relative; }
10 |
11 | .pickadate-main {
12 | margin: 0;
13 | padding: 0;
14 | width: 100%;
15 | text-align: center;
16 | font-size: 12px; }
17 |
18 | .pickadate-cell {
19 | overflow: hidden;
20 | margin: 0;
21 | padding: 0; }
22 | .pickadate-cell li {
23 | display: block;
24 | float: left;
25 | border: 1px solid #DCDCDC;
26 | border-width: 0 1px 1px 0;
27 | width: 14.285%;
28 | padding: 1.3% 0 1.3% 0;
29 | line-height: normal;
30 | box-sizing: border-box;
31 | -moz-box-sizing: border-box;
32 | -webkit-box-sizing: border-box; }
33 | .pickadate-cell li:nth-child(7n+0) {
34 | border-right: 1px solid #DCDCDC; }
35 | .pickadate-cell li:nth-child(1), .pickadate-cell li:nth-child(8), .pickadate-cell li:nth-child(15), .pickadate-cell li:nth-child(22), .pickadate-cell li:nth-child(29), .pickadate-cell li:nth-child(36) {
36 | border-left: 1px solid #DCDCDC; }
37 | .pickadate-cell .pickadate-disabled {
38 | color: #DCDCDC; }
39 | .pickadate-cell .pickadate-disabled a {
40 | color: #DCDCDC; }
41 | .pickadate-cell .pickadate-enabled {
42 | cursor: pointer;
43 | font-size: 12px;
44 | color: #666666; }
45 | .pickadate-cell .pickadate-today {
46 | background-color: #eaeaea; }
47 | .pickadate-cell .pickadate-active {
48 | background-color: #b52a00;
49 | color: white; }
50 | .pickadate-cell .pickadate-head {
51 | border-top: 1px solid #DCDCDC;
52 | background: #f3f3f3; }
53 | .pickadate-cell .pickadate-head:nth-child(1), .pickadate-cell .pickadate-head:nth-child(7) {
54 | background: #f3f3f3; }
55 |
56 | .pickadate-centered-heading {
57 | font-weight: normal;
58 | text-align: center;
59 | font-size: 1em;
60 | margin: 13px 0 13px 0;
61 | line-height: normal; }
62 |
63 | .pickadate-controls {
64 | position: absolute;
65 | z-index: 10;
66 | width: 100%; }
67 | .pickadate-controls .pickadate-next {
68 | float: right; }
69 | .pickadate-controls a {
70 | text-decoration: none;
71 | font-size: 0.9em; }
72 |
73 | .pickadate-modal {
74 | position: absolute;
75 | background-color: #fff;
76 | width: 300px;
77 | border: 1px solid #ccc;
78 | border-radius: 4px;
79 | padding: 0 5px 5px 5px;
80 | z-index: 1000; }
81 |
--------------------------------------------------------------------------------
/dist/angular-pickadate.js:
--------------------------------------------------------------------------------
1 | ;(function(angular){
2 | 'use strict';
3 | var indexOf = [].indexOf || function(item) {
4 | for (var i = 0, l = this.length; i < l; i++) {
5 | if (i in this && this[i] === item) return i;
6 | }
7 | return -1;
8 | };
9 |
10 | function map(items, property) {
11 | var mappedArray = [];
12 | angular.forEach(items, function(item) {
13 | mappedArray.push(angular.isFunction(property) ? property(item) : item[property]);
14 | });
15 | return mappedArray;
16 | }
17 |
18 | angular.module('pickadate', [])
19 |
20 | .provider('pickadateI18n', function() {
21 | var defaults = {
22 | 'prev': 'prev',
23 | 'next': 'next'
24 | };
25 |
26 | this.translations = {};
27 |
28 | this.$get = function() {
29 | var translations = this.translations;
30 |
31 | return {
32 | t: function(key) {
33 | return translations[key] || defaults[key];
34 | }
35 | };
36 | };
37 | })
38 |
39 | .factory('pickadateModalBindings', ['$window', '$document', function($window, $document) {
40 | var supportPageOffset = $window.pageXOffset !== undefined,
41 | isCSS1Compat = (($document.compatMode || "") === "CSS1Compat");
42 |
43 | var computeStyles = function(element) {
44 | var scrollX = supportPageOffset ? $window.pageXOffset : isCSS1Compat ? $document.documentElement.scrollLeft : $document.body.scrollLeft,
45 | scrollY = supportPageOffset ? $window.pageYOffset : isCSS1Compat ? $document.documentElement.scrollTop : $document.body.scrollTop,
46 | innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth,
47 | styles = { top: scrollY + element.getBoundingClientRect().bottom + 'px' };
48 |
49 | if ((innerWidth - element.getBoundingClientRect().left ) >= 300) {
50 | styles.left = scrollX + element.getBoundingClientRect().left + 'px';
51 | } else {
52 | styles.right = innerWidth - element.getBoundingClientRect().right - scrollX + 'px';
53 | }
54 |
55 | return styles;
56 | };
57 |
58 | var isDescendant = function(parent, child) {
59 | var node = child.parentNode;
60 | while (node !== null) {
61 | if (node === parent) return true;
62 | node = node.parentNode;
63 | }
64 | return false;
65 | };
66 |
67 | return function(scope, element, rootNode) {
68 | var togglePicker = function(toggle) {
69 | scope.displayPicker = toggle;
70 | scope.$apply();
71 | };
72 |
73 | element.on('focus', function() {
74 | scope.modalStyles = computeStyles(element[0]);
75 | togglePicker(true);
76 | });
77 |
78 | element.on('keydown', function(e) {
79 | if (indexOf.call([9, 13, 27], e.keyCode) >= 0) togglePicker(false);
80 | });
81 |
82 | $document.on('click', function(e) {
83 | if (isDescendant(rootNode, e.target) || e.target === element[0]) return;
84 | togglePicker(false);
85 | });
86 | };
87 |
88 | }])
89 |
90 | .factory('pickadateDateHelper', ['$locale', 'dateFilter', function($locale, dateFilter) {
91 |
92 | function getPartName(part) {
93 | switch (part) {
94 | case 'dd': return 'day';
95 | case 'MM': return 'month';
96 | case 'yyyy': return 'year';
97 | }
98 | }
99 |
100 | return function(format, options) {
101 | var minDate, maxDate, disabledDates, currentDate, weekStartsOn, noExtraRows;
102 |
103 | options = options || {};
104 | format = format || 'yyyy-MM-dd';
105 | weekStartsOn = options.weekStartsOn;
106 | noExtraRows = options.noExtraRows;
107 | disabledDates = options.disabledDates || angular.noop;
108 |
109 | if (!angular.isNumber(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) weekStartsOn = 0;
110 |
111 | return {
112 |
113 | parseDate: function(dateString) {
114 | if (!dateString) return;
115 | if (angular.isDate(dateString)) return new Date(dateString);
116 |
117 | var formatRegex = '(dd|MM|yyyy)',
118 | separator = format.match(/[-|/]/)[0],
119 | dateParts = dateString.split(separator),
120 | regexp = new RegExp([formatRegex, formatRegex, formatRegex].join(separator)),
121 | formatParts = format.match(regexp),
122 | dateObj = {};
123 |
124 | formatParts.shift();
125 |
126 | angular.forEach(formatParts, function(part, i) {
127 | dateObj[getPartName(part)] = parseInt(dateParts[i], 10);
128 | });
129 |
130 | if (isNaN(dateObj.year) || isNaN(dateObj.month) || isNaN(dateObj.day)) return;
131 |
132 | return new Date(dateObj.year, dateObj.month - 1, dateObj.day, 3);
133 | },
134 |
135 | setRestrictions: function(restrictions) {
136 | minDate = this.parseDate(restrictions.minDate) || new Date(0);
137 | maxDate = this.parseDate(restrictions.maxDate) || new Date(99999999999999);
138 | currentDate = restrictions.currentDate;
139 | },
140 |
141 | allowPrevMonth: function() {
142 | return currentDate > minDate;
143 | },
144 |
145 | allowNextMonth: function() {
146 | var nextMonth = angular.copy(currentDate);
147 | nextMonth.setMonth(nextMonth.getMonth() + 1);
148 | return nextMonth <= maxDate;
149 | },
150 |
151 | buildDateObject: function(date) {
152 | var localDate = angular.copy(date),
153 | formattedDate = dateFilter(localDate, format),
154 | disabled = disabledDates({date: localDate, formattedDate: formattedDate}),
155 | monthOffset = this.getMonthOffset(localDate, currentDate),
156 | outOfMinRange = localDate < minDate,
157 | outOfMaxRange = localDate > maxDate,
158 | outOfMonth = (monthOffset === -1 && !options.previousMonthSelectable) ||
159 | (monthOffset === 1 && !options.nextMonthSelectable);
160 |
161 | return {
162 | date: localDate,
163 | formattedDate: formattedDate,
164 | today: formattedDate === dateFilter(new Date(), format),
165 | disabled: disabled,
166 | outOfMinRange: outOfMinRange,
167 | outOfMaxRange: outOfMaxRange,
168 | monthOffset: monthOffset,
169 | enabled: !(disabled || outOfMinRange || outOfMaxRange || outOfMonth)
170 | };
171 | },
172 |
173 | buildDates: function(year, month, options) {
174 | var dates = [],
175 | date = new Date(year, month, 1, 3),
176 | lastDate = new Date(year, month + 1, 0, 3);
177 |
178 | options = options || {};
179 | currentDate = angular.copy(date);
180 |
181 | while (date.getDay() !== weekStartsOn) date.setDate(date.getDate() - 1);
182 |
183 | for (var i = 0; i < 42; i++) { // 42 == 6 rows of dates
184 | if (noExtraRows && date.getDay() === weekStartsOn && date > lastDate) break;
185 |
186 | dates.push(this.buildDateObject(date));
187 | date.setDate(date.getDate() + 1);
188 | }
189 |
190 | return dates;
191 | },
192 |
193 | buildDayNames: function() {
194 | var dayNames = $locale.DATETIME_FORMATS.SHORTDAY;
195 |
196 | if (weekStartsOn) {
197 | dayNames = dayNames.slice(0);
198 | for (var i = 0; i < weekStartsOn; i++) dayNames.push(dayNames.shift());
199 | }
200 | return dayNames;
201 | },
202 |
203 | getMonthOffset: function(date1, date2) {
204 | return date1.getMonth() - date2.getMonth() + (12 * (date1.getFullYear() - date2.getFullYear()));
205 | }
206 | };
207 | };
208 |
209 | }])
210 |
211 | .directive('pickadate', ['$locale', '$sce', '$compile', '$document', '$window', 'pickadateDateHelper',
212 | 'pickadateI18n', 'pickadateModalBindings', 'filterFilter', function($locale, $sce, $compile, $document, $window,
213 | dateHelperFactory, i18n, modalBindings, filter) {
214 |
215 | var TEMPLATE =
216 | '' +
217 | '' +
230 | '
' +
231 | '
' +
232 | '
' +
233 | '- ' +
234 | '{{dayName}}' +
235 | '
' +
236 | '
' +
237 | '
' +
238 | '- ' +
239 | '{{dateObj.date | date:"d"}}' +
240 | '
' +
241 | '
' +
242 | '
' +
243 | '
' +
244 | '
';
245 |
246 | return {
247 | require: 'ngModel',
248 | scope: {
249 | defaultDate: '=',
250 | minDate: '=',
251 | maxDate: '=',
252 | disabledDates: '&',
253 | weekStartsOn: '='
254 | },
255 |
256 | link: function(scope, element, attrs, ngModel) {
257 | var allowMultiple = attrs.hasOwnProperty('multiple'),
258 | allowBlank = attrs.hasOwnProperty('allowBlankDate'),
259 | selectedDates = [],
260 | wantsModal = element[0] instanceof HTMLInputElement,
261 | compiledHtml = $compile(TEMPLATE)(scope),
262 | format = (attrs.format || 'yyyy-MM-dd').replace(/m/g, 'M'),
263 | dateHelper = dateHelperFactory(format, {
264 | previousMonthSelectable: /^(previous|both)$/.test(attrs.selectOtherMonths),
265 | nextMonthSelectable: /^(next|both)$/.test(attrs.selectOtherMonths),
266 | weekStartsOn: scope.weekStartsOn,
267 | noExtraRows: attrs.hasOwnProperty('noExtraRows'),
268 | disabledDates: scope.disabledDates
269 | });
270 |
271 | scope.displayPicker = !wantsModal;
272 |
273 | scope.setDate = function(dateObj) {
274 | if (!dateObj.enabled) return;
275 |
276 | selectedDates = toggleDate(dateObj, selectedDates);
277 |
278 | setViewValue(selectedDates);
279 |
280 | scope.changeMonth(dateObj.monthOffset);
281 | scope.displayPicker = !wantsModal;
282 | };
283 |
284 | var $render = ngModel.$render = function(options) {
285 | if (angular.isArray(ngModel.$viewValue)) {
286 | selectedDates = ngModel.$viewValue;
287 | } else if (ngModel.$viewValue) {
288 | selectedDates = [ngModel.$viewValue];
289 | }
290 |
291 | scope.currentDate = dateHelper.parseDate(scope.defaultDate || selectedDates[0]) || new Date();
292 |
293 | dateHelper.setRestrictions(scope);
294 |
295 | selectedDates = map(selectedDates, function(date) {
296 | return dateHelper.buildDateObject(dateHelper.parseDate(date));
297 | });
298 |
299 | selectedDates = filter(selectedDates, { enabled: true });
300 |
301 | setViewValue(selectedDates, options);
302 | render();
303 | };
304 |
305 | scope.classesFor = function(date) {
306 | var formattedDates = map(selectedDates, 'formattedDate'),
307 | classes = indexOf.call(formattedDates, date.formattedDate) >= 0 ? 'pickadate-active' : null;
308 | return date.classNames.concat(classes);
309 | };
310 |
311 | scope.changeMonth = function(offset) {
312 | if (!offset) return;
313 | // If the current date is January 31th, setting the month to date.getMonth() + 1
314 | // sets the date to March the 3rd, since the date object adds 30 days to the current
315 | // date. Settings the date to the 2nd day of the month is a workaround to prevent this
316 | // behaviour
317 | scope.currentDate.setDate(1);
318 | scope.currentDate.setMonth(scope.currentDate.getMonth() + offset);
319 | render();
320 | };
321 |
322 | // Workaround to watch multiple properties. XXX use $scope.$watchGroup in angular 1.3
323 | scope.$watch(function() {
324 | return angular.toJson([scope.minDate, scope.maxDate]);
325 | }, $render);
326 |
327 | // Insert datepicker into DOM
328 | if (wantsModal) {
329 | modalBindings(scope, element, compiledHtml[0]);
330 |
331 | // if the user types a date, update the picker and set validity
332 | scope.$watch(function() {
333 | return ngModel.$viewValue;
334 | }, function(val) {
335 | var isValidDate = dateHelper.parseDate(val);
336 |
337 | if (isValidDate) $render({ skipRenderInput: true });
338 | ngModel.$setValidity('date', !!isValidDate);
339 | });
340 |
341 | // if the input element has a value, set it as the ng-model
342 | scope.$$postDigest(function() {
343 | if (attrs.value) { ngModel.$viewValue = attrs.value; $render(); }
344 | });
345 |
346 | element.after(compiledHtml.addClass('pickadate-modal'));
347 | } else {
348 | element.append(compiledHtml);
349 | }
350 |
351 | function render() {
352 | var dates = dateHelper.buildDates(scope.currentDate.getFullYear(), scope.currentDate.getMonth());
353 |
354 | scope.allowPrevMonth = dateHelper.allowPrevMonth();
355 | scope.allowNextMonth = dateHelper.allowNextMonth();
356 | scope.dayNames = dateHelper.buildDayNames();
357 |
358 | scope.dates = map(dates, function(date) {
359 | date.classNames = [date.enabled ? 'pickadate-enabled' : 'pickadate-disabled'];
360 |
361 | if (date.today) date.classNames.push('pickadate-today');
362 | if (date.disabled) date.classNames.push('pickadate-unavailable');
363 |
364 | return date;
365 | });
366 | }
367 |
368 | function setViewValue(value, options) {
369 | options = options || {};
370 |
371 | if (allowMultiple) {
372 | ngModel.$setViewValue(map(value, 'formattedDate'));
373 | } else {
374 | ngModel.$setViewValue(value[0] && value[0].formattedDate);
375 | }
376 |
377 | if (!options.skipRenderInput) element.val(ngModel.$viewValue);
378 | }
379 |
380 | function toggleDate(dateObj, dateArray) {
381 | var index = indexOf.call(map(dateArray, 'formattedDate'), dateObj.formattedDate);
382 | if (index === -1) {
383 | dateArray = addDate(dateObj, dateArray);
384 | } else if (allowBlank || (dateArray.length > 1)) {
385 | dateArray.splice(index, 1);
386 | }
387 | return dateArray;
388 | }
389 |
390 | function addDate(dateObj, dateArray){
391 | if (allowMultiple) {
392 | dateArray.push(dateObj);
393 | } else {
394 | dateArray = [dateObj];
395 | }
396 |
397 | return dateArray;
398 | }
399 | }
400 | };
401 | }]);
402 | })(window.angular);
403 |
--------------------------------------------------------------------------------
/dist/angular-pickadate.min.js:
--------------------------------------------------------------------------------
1 | !function(e){"use strict";function t(t,a){var n=[];return e.forEach(t,function(t){n.push(e.isFunction(a)?a(t):t[a])}),n}var a=[].indexOf||function(e){for(var t=0,a=this.length;t=300?l.left=r+a.getBoundingClientRect().left+"px":l.right=s-a.getBoundingClientRect().right-r+"px",l},o=function(e,t){for(var a=t.parentNode;null!==a;){if(a===e)return!0;a=a.parentNode}return!1};return function(e,n,i){var s=function(t){e.displayPicker=t,e.$apply()};n.on("focus",function(){e.modalStyles=r(n[0]),s(!0)}),n.on("keydown",function(e){a.call([9,13,27],e.keyCode)>=0&&s(!1)}),t.on("click",function(e){o(i,e.target)||e.target===n[0]||s(!1)})}}]).factory("pickadateDateHelper",["$locale","dateFilter",function(t,a){function n(e){switch(e){case"dd":return"day";case"MM":return"month";case"yyyy":return"year"}}return function(i,r){var o,s,l,c,d,u;return r=r||{},i=i||"yyyy-MM-dd",d=r.weekStartsOn,u=r.noExtraRows,l=r.disabledDates||e.noop,(!e.isNumber(d)||d<0||d>6)&&(d=0),{parseDate:function(t){if(t){if(e.isDate(t))return new Date(t);var a="(dd|MM|yyyy)",r=i.match(/[-|\/]/)[0],o=t.split(r),s=new RegExp([a,a,a].join(r)),l=i.match(s),c={};if(l.shift(),e.forEach(l,function(e,t){c[n(e)]=parseInt(o[t],10)}),!(isNaN(c.year)||isNaN(c.month)||isNaN(c.day)))return new Date(c.year,c.month-1,c.day,3)}},setRestrictions:function(e){o=this.parseDate(e.minDate)||new Date(0),s=this.parseDate(e.maxDate)||new Date(99999999999999),c=e.currentDate},allowPrevMonth:function(){return c>o},allowNextMonth:function(){var t=e.copy(c);return t.setMonth(t.getMonth()+1),t<=s},buildDateObject:function(t){var n=e.copy(t),d=a(n,i),u=l({date:n,formattedDate:d}),f=this.getMonthOffset(n,c),p=ns,D=f===-1&&!r.previousMonthSelectable||1===f&&!r.nextMonthSelectable;return{date:n,formattedDate:d,today:d===a(new Date,i),disabled:u,outOfMinRange:p,outOfMaxRange:h,monthOffset:f,enabled:!(u||p||h||D)}},buildDates:function(t,a,n){var i=[],r=new Date(t,a,1,3),o=new Date(t,a+1,0,3);for(n=n||{},c=e.copy(r);r.getDay()!==d;)r.setDate(r.getDate()-1);for(var s=0;s<42&&!(u&&r.getDay()===d&&r>o);s++)i.push(this.buildDateObject(r)),r.setDate(r.getDate()+1);return i},buildDayNames:function(){var e=t.DATETIME_FORMATS.SHORTDAY;if(d){e=e.slice(0);for(var a=0;a'+i.trustAsHtml(c.t("next"))+'{{currentDate | date:"MMMM yyyy"}}
- {{dateObj.date | date:"d"}}
';return{require:"ngModel",scope:{defaultDate:"=",minDate:"=",maxDate:"=",disabledDates:"&",weekStartsOn:"="},link:function(n,i,o,s){function c(){var e=k.buildDates(n.currentDate.getFullYear(),n.currentDate.getMonth());n.allowPrevMonth=k.allowPrevMonth(),n.allowNextMonth=k.allowNextMonth(),n.dayNames=k.buildDayNames(),n.dates=t(e,function(e){return e.classNames=[e.enabled?"pickadate-enabled":"pickadate-disabled"],e.today&&e.classNames.push("pickadate-today"),e.disabled&&e.classNames.push("pickadate-unavailable"),e})}function p(e,a){a=a||{},y?s.$setViewValue(t(e,"formattedDate")):s.$setViewValue(e[0]&&e[0].formattedDate),a.skipRenderInput||i.val(s.$viewValue)}function h(e,n){var i=a.call(t(n,"formattedDate"),e.formattedDate);return i===-1?n=D(e,n):(v||n.length>1)&&n.splice(i,1),n}function D(e,t){return y?t.push(e):t=[e],t}var y=o.hasOwnProperty("multiple"),v=o.hasOwnProperty("allowBlankDate"),g=[],m=i[0]instanceof HTMLInputElement,w=r(f)(n),M=(o.format||"yyyy-MM-dd").replace(/m/g,"M"),k=l(M,{previousMonthSelectable:/^(previous|both)$/.test(o.selectOtherMonths),nextMonthSelectable:/^(next|both)$/.test(o.selectOtherMonths),weekStartsOn:n.weekStartsOn,noExtraRows:o.hasOwnProperty("noExtraRows"),disabledDates:n.disabledDates});n.displayPicker=!m,n.setDate=function(e){e.enabled&&(g=h(e,g),p(g),n.changeMonth(e.monthOffset),n.displayPicker=!m)};var b=s.$render=function(a){e.isArray(s.$viewValue)?g=s.$viewValue:s.$viewValue&&(g=[s.$viewValue]),n.currentDate=k.parseDate(n.defaultDate||g[0])||new Date,k.setRestrictions(n),g=t(g,function(e){return k.buildDateObject(k.parseDate(e))}),g=u(g,{enabled:!0}),p(g,a),c()};n.classesFor=function(e){var n=t(g,"formattedDate"),i=a.call(n,e.formattedDate)>=0?"pickadate-active":null;return e.classNames.concat(i)},n.changeMonth=function(e){e&&(n.currentDate.setDate(1),n.currentDate.setMonth(n.currentDate.getMonth()+e),c())},n.$watch(function(){return e.toJson([n.minDate,n.maxDate])},b),m?(d(n,i,w[0]),n.$watch(function(){return s.$viewValue},function(e){var t=k.parseDate(e);t&&b({skipRenderInput:!0}),s.$setValidity("date",!!t)}),n.$$postDigest(function(){o.value&&(s.$viewValue=o.value,b())}),i.after(w.addClass("pickadate-modal"))):i.append(w)}}}])}(window.angular);
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /* jshint strict: false, node: true */
2 |
3 | var gulp = require('gulp');
4 | var sass = require('gulp-sass');
5 | var uglify = require('gulp-uglify');
6 | var rename = require("gulp-rename");
7 | var del = require("del");
8 | var _ = require('lodash');
9 | var karma = require('karma').server;
10 | var karmaConf = require('./karma.conf');
11 | var jshint = require('gulp-jshint');
12 | var legacyVersions = ['1.2.21', '1.3.6'];
13 |
14 | var karmaConfFor = function(version) {
15 | var conf = _.clone(karmaConf);
16 | conf.files = _.clone(karmaConf.files);
17 | conf.files.unshift('test/lib/angular-*' + version + '.js');
18 | return conf;
19 | };
20 |
21 | gulp.task('clean', function(done) {
22 | del('dist/*', done);
23 | });
24 |
25 | gulp.task('dist', ['uglify', 'sass'], function() {
26 | return gulp.src('./src/*.js')
27 | .pipe(gulp.dest('./dist'));
28 | });
29 |
30 | gulp.task('uglify', function() {
31 | gulp.src('./src/*.js')
32 | .pipe(uglify())
33 | .pipe(rename({suffix: '.min'}))
34 | .pipe(gulp.dest('./dist'));
35 | });
36 |
37 | gulp.task('sass', function () {
38 | gulp.src('./src/*.scss')
39 | .pipe(sass({ errLogToConsole: true }))
40 | .pipe(gulp.dest('./dist'));
41 | });
42 |
43 | gulp.task('lint', function() {
44 | return gulp.src('./src/angular-pickadate.js')
45 | .pipe(jshint())
46 | .pipe(jshint.reporter('default'))
47 | .pipe(jshint.reporter('fail'));
48 | });
49 |
50 | legacyVersions.forEach(function(version) {
51 | gulp.task('test:legacy:' + version, function (done) {
52 | karma.start(_.assign({}, karmaConfFor(version), {singleRun: true}), done);
53 | });
54 |
55 | gulp.task('tdd:legacy:' + version, function (done) {
56 | karma.start(karmaConfFor(version), done);
57 | });
58 | });
59 |
60 | gulp.task('test:legacy', legacyVersions.map(function(version) {
61 | return 'test:legacy:' + version;
62 | }));
63 |
64 | /**
65 | * Run test once and exit
66 | */
67 | gulp.task('test', ['lint', 'test:legacy'], function (done) {
68 | karma.start(_.assign({}, karmaConfFor('1.4.0'), {singleRun: true}), done);
69 | });
70 |
71 |
72 | gulp.task('tdd', function (done) {
73 | karma.start(karmaConfFor('1.4.0'), done);
74 | });
75 |
76 | gulp.task('default', ['tdd']);
77 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | require('karma-chai-plugins');
2 |
3 | // Karma configuration
4 | // Generated on Sat Aug 09 2014 18:30:57 GMT-0300 (ART)
5 |
6 | module.exports = {
7 |
8 | // base path that will be used to resolve all patterns (eg. files, exclude)
9 | basePath: '',
10 |
11 |
12 | // frameworks to use
13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
14 | frameworks: ['mocha', 'chai', 'chai-jquery', 'sinon-chai'],
15 |
16 |
17 | // list of files / patterns to load in the browser
18 | files: [
19 | 'test/lib/browser_trigger.js',
20 | 'src/angular-pickadate.js',
21 | 'node_modules/jquery/dist/jquery.js',
22 | 'test/**/*.spec.js'
23 | ],
24 |
25 |
26 | // list of files to exclude
27 | exclude: [
28 | ],
29 |
30 |
31 | // preprocess matching files before serving them to the browser
32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
33 | preprocessors: {
34 | },
35 |
36 |
37 | // test results reporter to use
38 | // possible values: 'dots', 'progress'
39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
40 | reporters: ['progress'],
41 |
42 |
43 | // web server port
44 | port: 9876,
45 |
46 |
47 | // enable / disable colors in the output (reporters and logs)
48 | colors: true,
49 |
50 | // start these browsers
51 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
52 | browsers: ['PhantomJS'],
53 |
54 | plugins: [
55 | 'karma-mocha',
56 | require('karma-phantomjs-launcher'),
57 | require('karma-chai-plugins')
58 | ]
59 | };
60 |
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-pickadate",
3 | "version": "2.0.2",
4 | "description": "A simple and fluid inline datepicker for AngularJS with no extra dependencies",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "gulp test"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/restorando/angular-pickadate.git"
12 | },
13 | "keywords": [
14 | "datepicker",
15 | "pickadate",
16 | "angular"
17 | ],
18 | "author": "Gabriel Schammah",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/restorando/angular-pickadate/issues"
22 | },
23 | "homepage": "https://github.com/restorando/angular-pickadate",
24 | "devDependencies": {
25 | "del": "^2.2.0",
26 | "gulp": "^3.9.1",
27 | "gulp-jshint": "^2.0.0",
28 | "gulp-rename": "^1.2.2",
29 | "gulp-sass": "^2.3.2",
30 | "gulp-uglify": "^1.5.4",
31 | "jquery": "^2.2.3",
32 | "jshint": "^2.9.1",
33 | "karma": "^0.13.22",
34 | "karma-chai-plugins": "^0.7.0",
35 | "karma-mocha": "^0.2.2",
36 | "karma-phantomjs-launcher": "^1.0.0",
37 | "lodash": "^4.11.1",
38 | "mocha": "^2.4.5",
39 | "phantomjs-prebuilt": "^2.1.7"
40 | },
41 | "dependencies": {
42 | "del": "^2.2.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/angular-pickadate.js:
--------------------------------------------------------------------------------
1 | ;(function(angular){
2 | 'use strict';
3 | var indexOf = [].indexOf || function(item) {
4 | for (var i = 0, l = this.length; i < l; i++) {
5 | if (i in this && this[i] === item) return i;
6 | }
7 | return -1;
8 | };
9 |
10 | function map(items, property) {
11 | var mappedArray = [];
12 | angular.forEach(items, function(item) {
13 | mappedArray.push(angular.isFunction(property) ? property(item) : item[property]);
14 | });
15 | return mappedArray;
16 | }
17 |
18 | angular.module('pickadate', [])
19 |
20 | .provider('pickadateI18n', function() {
21 | var defaults = {
22 | 'prev': 'prev',
23 | 'next': 'next'
24 | };
25 |
26 | this.translations = {};
27 |
28 | this.$get = function() {
29 | var translations = this.translations;
30 |
31 | return {
32 | t: function(key) {
33 | return translations[key] || defaults[key];
34 | }
35 | };
36 | };
37 | })
38 |
39 | .factory('pickadateModalBindings', ['$window', '$document', function($window, $document) {
40 | var supportPageOffset = $window.pageXOffset !== undefined,
41 | isCSS1Compat = (($document.compatMode || "") === "CSS1Compat");
42 |
43 | var computeStyles = function(element) {
44 | var scrollX = supportPageOffset ? $window.pageXOffset : isCSS1Compat ? $document.documentElement.scrollLeft : $document.body.scrollLeft,
45 | scrollY = supportPageOffset ? $window.pageYOffset : isCSS1Compat ? $document.documentElement.scrollTop : $document.body.scrollTop,
46 | innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth,
47 | styles = { top: scrollY + element.getBoundingClientRect().bottom + 'px' };
48 |
49 | if ((innerWidth - element.getBoundingClientRect().left ) >= 300) {
50 | styles.left = scrollX + element.getBoundingClientRect().left + 'px';
51 | } else {
52 | styles.right = innerWidth - element.getBoundingClientRect().right - scrollX + 'px';
53 | }
54 |
55 | return styles;
56 | };
57 |
58 | var isDescendant = function(parent, child) {
59 | var node = child.parentNode;
60 | while (node !== null) {
61 | if (node === parent) return true;
62 | node = node.parentNode;
63 | }
64 | return false;
65 | };
66 |
67 | return function(scope, element, rootNode) {
68 | var togglePicker = function(toggle) {
69 | scope.displayPicker = toggle;
70 | scope.$apply();
71 | };
72 |
73 | element.on('focus', function() {
74 | scope.modalStyles = computeStyles(element[0]);
75 | togglePicker(true);
76 | });
77 |
78 | element.on('keydown', function(e) {
79 | if (indexOf.call([9, 13, 27], e.keyCode) >= 0) togglePicker(false);
80 | });
81 |
82 | $document.on('click', function(e) {
83 | if (isDescendant(rootNode, e.target) || e.target === element[0]) return;
84 | togglePicker(false);
85 | });
86 | };
87 |
88 | }])
89 |
90 | .factory('pickadateDateHelper', ['$locale', 'dateFilter', function($locale, dateFilter) {
91 |
92 | function getPartName(part) {
93 | switch (part) {
94 | case 'dd': return 'day';
95 | case 'MM': return 'month';
96 | case 'yyyy': return 'year';
97 | }
98 | }
99 |
100 | return function(format, options) {
101 | var minDate, maxDate, disabledDates, currentDate, weekStartsOn, noExtraRows;
102 |
103 | options = options || {};
104 | format = format || 'yyyy-MM-dd';
105 | weekStartsOn = options.weekStartsOn;
106 | noExtraRows = options.noExtraRows;
107 | disabledDates = options.disabledDates || angular.noop;
108 |
109 | if (!angular.isNumber(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) weekStartsOn = 0;
110 |
111 | return {
112 |
113 | parseDate: function(dateString) {
114 | if (!dateString) return;
115 | if (angular.isDate(dateString)) return new Date(dateString);
116 |
117 | var formatRegex = '(dd|MM|yyyy)',
118 | separator = format.match(/[-|/]/)[0],
119 | dateParts = dateString.split(separator),
120 | regexp = new RegExp([formatRegex, formatRegex, formatRegex].join(separator)),
121 | formatParts = format.match(regexp),
122 | dateObj = {};
123 |
124 | formatParts.shift();
125 |
126 | angular.forEach(formatParts, function(part, i) {
127 | dateObj[getPartName(part)] = parseInt(dateParts[i], 10);
128 | });
129 |
130 | if (isNaN(dateObj.year) || isNaN(dateObj.month) || isNaN(dateObj.day)) return;
131 |
132 | return new Date(dateObj.year, dateObj.month - 1, dateObj.day, 3);
133 | },
134 |
135 | setRestrictions: function(restrictions) {
136 | minDate = this.parseDate(restrictions.minDate) || new Date(0);
137 | maxDate = this.parseDate(restrictions.maxDate) || new Date(99999999999999);
138 | currentDate = restrictions.currentDate;
139 | },
140 |
141 | allowPrevMonth: function() {
142 | return currentDate > minDate;
143 | },
144 |
145 | allowNextMonth: function() {
146 | var nextMonth = angular.copy(currentDate);
147 | nextMonth.setMonth(nextMonth.getMonth() + 1);
148 | return nextMonth <= maxDate;
149 | },
150 |
151 | buildDateObject: function(date) {
152 | var localDate = angular.copy(date),
153 | formattedDate = dateFilter(localDate, format),
154 | disabled = disabledDates({date: localDate, formattedDate: formattedDate}),
155 | monthOffset = this.getMonthOffset(localDate, currentDate),
156 | outOfMinRange = localDate < minDate,
157 | outOfMaxRange = localDate > maxDate,
158 | outOfMonth = (monthOffset === -1 && !options.previousMonthSelectable) ||
159 | (monthOffset === 1 && !options.nextMonthSelectable);
160 |
161 | return {
162 | date: localDate,
163 | formattedDate: formattedDate,
164 | today: formattedDate === dateFilter(new Date(), format),
165 | disabled: disabled,
166 | outOfMinRange: outOfMinRange,
167 | outOfMaxRange: outOfMaxRange,
168 | monthOffset: monthOffset,
169 | enabled: !(disabled || outOfMinRange || outOfMaxRange || outOfMonth)
170 | };
171 | },
172 |
173 | buildDates: function(year, month, options) {
174 | var dates = [],
175 | date = new Date(year, month, 1, 3),
176 | lastDate = new Date(year, month + 1, 0, 3);
177 |
178 | options = options || {};
179 | currentDate = angular.copy(date);
180 |
181 | while (date.getDay() !== weekStartsOn) date.setDate(date.getDate() - 1);
182 |
183 | for (var i = 0; i < 42; i++) { // 42 == 6 rows of dates
184 | if (noExtraRows && date.getDay() === weekStartsOn && date > lastDate) break;
185 |
186 | dates.push(this.buildDateObject(date));
187 | date.setDate(date.getDate() + 1);
188 | }
189 |
190 | return dates;
191 | },
192 |
193 | buildDayNames: function() {
194 | var dayNames = $locale.DATETIME_FORMATS.SHORTDAY;
195 |
196 | if (weekStartsOn) {
197 | dayNames = dayNames.slice(0);
198 | for (var i = 0; i < weekStartsOn; i++) dayNames.push(dayNames.shift());
199 | }
200 | return dayNames;
201 | },
202 |
203 | getMonthOffset: function(date1, date2) {
204 | return date1.getMonth() - date2.getMonth() + (12 * (date1.getFullYear() - date2.getFullYear()));
205 | }
206 | };
207 | };
208 |
209 | }])
210 |
211 | .directive('pickadate', ['$locale', '$sce', '$compile', '$document', '$window', 'pickadateDateHelper',
212 | 'pickadateI18n', 'pickadateModalBindings', 'filterFilter', function($locale, $sce, $compile, $document, $window,
213 | dateHelperFactory, i18n, modalBindings, filter) {
214 |
215 | var TEMPLATE =
216 | '' +
217 | '' +
230 | '
' +
231 | '
' +
232 | '
' +
233 | '- ' +
234 | '{{dayName}}' +
235 | '
' +
236 | '
' +
237 | '
' +
238 | '- ' +
239 | '{{dateObj.date | date:"d"}}' +
240 | '
' +
241 | '
' +
242 | '
' +
243 | '
' +
244 | '
';
245 |
246 | return {
247 | require: 'ngModel',
248 | scope: {
249 | defaultDate: '=',
250 | minDate: '=',
251 | maxDate: '=',
252 | disabledDates: '&',
253 | weekStartsOn: '='
254 | },
255 |
256 | link: function(scope, element, attrs, ngModel) {
257 | var allowMultiple = attrs.hasOwnProperty('multiple'),
258 | allowBlank = attrs.hasOwnProperty('allowBlankDate'),
259 | selectedDates = [],
260 | wantsModal = element[0] instanceof HTMLInputElement,
261 | compiledHtml = $compile(TEMPLATE)(scope),
262 | format = (attrs.format || 'yyyy-MM-dd').replace(/m/g, 'M'),
263 | dateHelper = dateHelperFactory(format, {
264 | previousMonthSelectable: /^(previous|both)$/.test(attrs.selectOtherMonths),
265 | nextMonthSelectable: /^(next|both)$/.test(attrs.selectOtherMonths),
266 | weekStartsOn: scope.weekStartsOn,
267 | noExtraRows: attrs.hasOwnProperty('noExtraRows'),
268 | disabledDates: scope.disabledDates
269 | });
270 |
271 | scope.displayPicker = !wantsModal;
272 |
273 | scope.setDate = function(dateObj) {
274 | if (!dateObj.enabled) return;
275 |
276 | selectedDates = toggleDate(dateObj, selectedDates);
277 |
278 | setViewValue(selectedDates);
279 |
280 | scope.changeMonth(dateObj.monthOffset);
281 | scope.displayPicker = !wantsModal;
282 | };
283 |
284 | var $render = ngModel.$render = function(options) {
285 | if (angular.isArray(ngModel.$viewValue)) {
286 | selectedDates = ngModel.$viewValue;
287 | } else if (ngModel.$viewValue) {
288 | selectedDates = [ngModel.$viewValue];
289 | }
290 |
291 | scope.currentDate = dateHelper.parseDate(scope.defaultDate || selectedDates[0]) || new Date();
292 |
293 | dateHelper.setRestrictions(scope);
294 |
295 | selectedDates = map(selectedDates, function(date) {
296 | return dateHelper.buildDateObject(dateHelper.parseDate(date));
297 | });
298 |
299 | selectedDates = filter(selectedDates, { enabled: true });
300 |
301 | setViewValue(selectedDates, options);
302 | render();
303 | };
304 |
305 | scope.classesFor = function(date) {
306 | var formattedDates = map(selectedDates, 'formattedDate'),
307 | classes = indexOf.call(formattedDates, date.formattedDate) >= 0 ? 'pickadate-active' : null;
308 | return date.classNames.concat(classes);
309 | };
310 |
311 | scope.changeMonth = function(offset) {
312 | if (!offset) return;
313 | // If the current date is January 31th, setting the month to date.getMonth() + 1
314 | // sets the date to March the 3rd, since the date object adds 30 days to the current
315 | // date. Settings the date to the 2nd day of the month is a workaround to prevent this
316 | // behaviour
317 | scope.currentDate.setDate(1);
318 | scope.currentDate.setMonth(scope.currentDate.getMonth() + offset);
319 | render();
320 | };
321 |
322 | // Workaround to watch multiple properties. XXX use $scope.$watchGroup in angular 1.3
323 | scope.$watch(function() {
324 | return angular.toJson([scope.minDate, scope.maxDate]);
325 | }, $render);
326 |
327 | // Insert datepicker into DOM
328 | if (wantsModal) {
329 | modalBindings(scope, element, compiledHtml[0]);
330 |
331 | // if the user types a date, update the picker and set validity
332 | scope.$watch(function() {
333 | return ngModel.$viewValue;
334 | }, function(val) {
335 | var isValidDate = dateHelper.parseDate(val);
336 |
337 | if (isValidDate) $render({ skipRenderInput: true });
338 | ngModel.$setValidity('date', !!isValidDate);
339 | });
340 |
341 | // if the input element has a value, set it as the ng-model
342 | scope.$$postDigest(function() {
343 | if (attrs.value) { ngModel.$viewValue = attrs.value; $render(); }
344 | });
345 |
346 | element.after(compiledHtml.addClass('pickadate-modal'));
347 | } else {
348 | element.append(compiledHtml);
349 | }
350 |
351 | function render() {
352 | var dates = dateHelper.buildDates(scope.currentDate.getFullYear(), scope.currentDate.getMonth());
353 |
354 | scope.allowPrevMonth = dateHelper.allowPrevMonth();
355 | scope.allowNextMonth = dateHelper.allowNextMonth();
356 | scope.dayNames = dateHelper.buildDayNames();
357 |
358 | scope.dates = map(dates, function(date) {
359 | date.classNames = [date.enabled ? 'pickadate-enabled' : 'pickadate-disabled'];
360 |
361 | if (date.today) date.classNames.push('pickadate-today');
362 | if (date.disabled) date.classNames.push('pickadate-unavailable');
363 |
364 | return date;
365 | });
366 | }
367 |
368 | function setViewValue(value, options) {
369 | options = options || {};
370 |
371 | if (allowMultiple) {
372 | ngModel.$setViewValue(map(value, 'formattedDate'));
373 | } else {
374 | ngModel.$setViewValue(value[0] && value[0].formattedDate);
375 | }
376 |
377 | if (!options.skipRenderInput) element.val(ngModel.$viewValue);
378 | }
379 |
380 | function toggleDate(dateObj, dateArray) {
381 | var index = indexOf.call(map(dateArray, 'formattedDate'), dateObj.formattedDate);
382 | if (index === -1) {
383 | dateArray = addDate(dateObj, dateArray);
384 | } else if (allowBlank || (dateArray.length > 1)) {
385 | dateArray.splice(index, 1);
386 | }
387 | return dateArray;
388 | }
389 |
390 | function addDate(dateObj, dateArray){
391 | if (allowMultiple) {
392 | dateArray.push(dateObj);
393 | } else {
394 | dateArray = [dateObj];
395 | }
396 |
397 | return dateArray;
398 | }
399 | }
400 | };
401 | }]);
402 | })(window.angular);
403 |
--------------------------------------------------------------------------------
/src/angular-pickadate.scss:
--------------------------------------------------------------------------------
1 | .pickadate {
2 | font-family: 'Helvetica Neue', Helvetica, Helvetica, Arial, sans-serif;
3 |
4 | a {
5 | &:visited {
6 | color: #666666;
7 | }
8 | color: #666666;
9 | }
10 | }
11 |
12 | .pickadate-header {
13 | position: relative;
14 | }
15 |
16 | .pickadate-main {
17 | margin: 0;
18 | padding: 0;
19 | width: 100%;
20 | text-align: center;
21 | font-size: 12px;
22 | }
23 |
24 | .pickadate-cell {
25 | overflow: hidden;
26 | margin: 0;
27 | padding: 0;
28 | li {
29 | display: block;
30 | float: left;
31 | border: 1px solid #DCDCDC;
32 | border-width: 0 1px 1px 0;
33 | width: 14.285%;
34 | padding: 1.3% 0 1.3% 0;
35 | line-height: normal;
36 | box-sizing: border-box;
37 | -moz-box-sizing: border-box;
38 | -webkit-box-sizing: border-box;
39 | &:nth-child(7n+0) {
40 | border-right: 1px solid #DCDCDC;
41 | }
42 | &:nth-child(1), &:nth-child(8), &:nth-child(15), &:nth-child(22), &:nth-child(29), &:nth-child(36) {
43 | border-left: 1px solid #DCDCDC;
44 | }
45 | }
46 | .pickadate-disabled {
47 | color: #DCDCDC;
48 | a {
49 | color: #DCDCDC;
50 | }
51 | }
52 | .pickadate-enabled {
53 | cursor: pointer;
54 | font-size: 12px;
55 | color: #666666;
56 | }
57 | .pickadate-today {
58 | background-color: #eaeaea;
59 | }
60 | .pickadate-active {
61 | background-color: #b52a00;
62 | color: white;
63 | }
64 | .pickadate-head {
65 | border-top: 1px solid #DCDCDC;
66 | background: #f3f3f3;
67 | &:nth-child(1), &:nth-child(7) {
68 | background: #f3f3f3;
69 | }
70 | }
71 | }
72 |
73 | .pickadate-centered-heading {
74 | font-weight: normal;
75 | text-align: center;
76 | font-size: 1em;
77 | margin: 13px 0 13px 0;
78 | line-height: normal;
79 | }
80 |
81 | .pickadate-controls {
82 | position: absolute;
83 | z-index: 10;
84 | width: 100%;
85 | .pickadate-next {
86 | float: right;
87 | }
88 | a {
89 | text-decoration: none;
90 | font-size: 0.9em;
91 | }
92 | }
93 |
94 | .pickadate-modal {
95 | position: absolute;
96 | background-color: #fff;
97 | width: 300px;
98 | border: 1px solid #ccc;
99 | border-radius: 4px;
100 | padding: 0 5px 5px 5px;
101 | z-index: 1000;
102 | }
103 |
--------------------------------------------------------------------------------
/test/angular-pickadate.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint expr: true */
2 |
3 | describe('pickadate', function () {
4 | 'use strict';
5 |
6 | var element,
7 | $scope,
8 | $compile,
9 | $document,
10 | pickadateI18nProvider,
11 | defaultHtml = '' +
15 | '
';
16 |
17 | beforeEach(module('pickadate'));
18 |
19 | beforeEach(module(['pickadateI18nProvider', function(_pickadateI18nProvider_) {
20 | pickadateI18nProvider = _pickadateI18nProvider_;
21 | }]));
22 |
23 | beforeEach(function() {
24 | inject(function($rootScope, _$compile_, _$document_){
25 | $scope = $rootScope.$new();
26 | $compile = _$compile_;
27 | $document = _$document_;
28 | $scope.disabledDatesFn = function(date, formattedDate) {
29 | return ($scope.disabledDates || []).indexOf(formattedDate) > -1;
30 | }
31 | });
32 | });
33 |
34 | function compile(html) {
35 | element = angular.element(html || defaultHtml);
36 | $compile(element)($scope);
37 | $scope.$digest();
38 | }
39 |
40 | function $(selector) {
41 | return jQuery(selector, element);
42 | }
43 |
44 | describe('Model binding', function() {
45 |
46 | beforeEach(function() {
47 | $scope.date = '2014-05-17';
48 | $scope.disabledDates = ['2014-05-26'];
49 | compile();
50 | });
51 |
52 | it("updates the ngModel value when a date is clicked", function() {
53 | expect($scope.date).to.equal('2014-05-17');
54 | browserTrigger($('.pickadate-enabled:contains(27)'), 'click');
55 | expect($scope.date).to.equal('2014-05-27');
56 | });
57 |
58 | it("doesn't allow an unavailable date to be clicked", function() {
59 | expect($scope.date).to.equal('2014-05-17');
60 | browserTrigger($('.pickadate-enabled:contains(26)'), 'click');
61 | expect($scope.date).to.equal('2014-05-17');
62 | });
63 |
64 | it("sets the ngModel as undefined if the model date is in the disabled list", function() {
65 | $scope.date = '2014-05-26';
66 | $scope.$digest();
67 | expect($scope.date).to.be.undefined;
68 | });
69 |
70 | });
71 |
72 | describe('Rendering', function() {
73 |
74 | beforeEach(function() {
75 | $scope.date = '2014-05-17';
76 | $scope.disabledDates = ['2014-05-26'];
77 | compile();
78 | });
79 |
80 | describe('Selected date', function() {
81 |
82 | it("doesn't add the pickadate-modal class", function() {
83 | expect($('.pickadate')).not.to.have.class('pickadate-modal');
84 | });
85 |
86 | it("adds the 'pickadate-active' class for the selected date", function() {
87 | expect($('.pickadate-active')).to.have.text('17');
88 | expect($('.pickadate-active').length).to.equal(1);
89 |
90 | browserTrigger($('.pickadate-enabled:contains(27)'), 'click');
91 |
92 | expect($('.pickadate-active')).to.have.text('27');
93 | expect($('.pickadate-active').length).to.equal(1);
94 | });
95 |
96 | it("doesn't have an element with the 'pickadate-active' class for the next month", function() {
97 | browserTrigger($('.pickadate-next'), 'click');
98 | expect($('.pickadate-active').length).to.be.empty;
99 | });
100 |
101 | it("doesn't change the active element when a disabled date is clicked", function() {
102 | browserTrigger($('.pickadate-enabled:contains(26)'), 'click');
103 | expect($('.pickadate-active')).to.have.text('17');
104 | expect($('.pickadate-active').length).to.equal(1);
105 | });
106 |
107 | });
108 |
109 | describe('Disabled dates', function() {
110 |
111 | describe('As an array', function() {
112 | beforeEach(function() {
113 | $scope.disabledDates = ['2014-05-20', '2014-05-26'];
114 | compile();
115 | });
116 |
117 | it("adds the 'pickadate-unavailable' class to the disabled dates", function() {
118 | expect($('li:contains(20)')).to.have.class('pickadate-unavailable');
119 | expect($('li:contains(26)')).to.have.class('pickadate-unavailable');
120 | });
121 |
122 | it("doesn't change the selected date if an unavailable one is clicked", function() {
123 | browserTrigger($('.pickadate-unavailable:contains(20)'), 'click');
124 | expect($scope.date).to.equal('2014-05-17');
125 | });
126 | });
127 |
128 | describe('As a function', function() {
129 | beforeEach(function() {
130 | $scope.disabledDatesFn = function(date) {
131 | return date.getDay() <= 1;
132 | }
133 | compile();
134 | });
135 |
136 | it("adds the 'pickadate-unavailable' class to the disabled dates", function() {
137 | expect($('li:contains(11)')).to.have.class('pickadate-unavailable');
138 | expect($('li:contains(12)')).to.have.class('pickadate-unavailable');
139 | expect($('li:contains(18)')).to.have.class('pickadate-unavailable');
140 | expect($('li:contains(19)')).to.have.class('pickadate-unavailable');
141 | expect($('li:contains(25)')).to.have.class('pickadate-unavailable');
142 | expect($('li:contains(26)')).to.have.class('pickadate-unavailable');
143 | });
144 | })
145 |
146 | });
147 |
148 | describe('Watchers', function() {
149 |
150 | describe('Min && max date', function() {
151 |
152 | beforeEach(function() {
153 | $scope.minDate = '2014-04-20';
154 | $scope.maxDate = '2014-06-20';
155 | $scope.date = '2014-05-17';
156 | compile();
157 | });
158 |
159 | it("re-renders the calendar if min-date is updated", function() {
160 | expect($('li:contains(14)')).not.to.have.class('pickadate-disabled');
161 |
162 | $scope.minDate = '2014-05-15';
163 | $scope.$digest();
164 |
165 | expect($('li:contains(14)')).to.have.class('pickadate-disabled');
166 | expect($('li:contains(15)')).not.to.have.class('pickadate-disabled');
167 | });
168 |
169 | it("re-renders the calendar if max-date is updated", function() {
170 | expect($('li:contains(23)')).not.to.have.class('pickadate-disabled');
171 |
172 | $scope.maxDate = '2014-05-22';
173 | $scope.$digest();
174 |
175 | expect($('li:contains(22)')).not.to.have.class('pickadate-disabled');
176 | expect($('li:contains(23)')).to.have.class('pickadate-disabled');
177 | });
178 |
179 | it("unselects the current date if it's not in the min-date - max-date range", function() {
180 | $scope.minDate = '2014-05-19';
181 | $scope.$digest();
182 |
183 | expect($scope.date).to.be.undefined;
184 | });
185 |
186 | it("re-renders the calendar on the selected date", function() {
187 | $scope.date = '2014-06-20';
188 | $scope.$digest();
189 |
190 | expect($('.pickadate-centered-heading')).to.have.text('June 2014');
191 | expect($('li:contains(20)')).to.have.class('pickadate-active');
192 | });
193 |
194 | });
195 |
196 | });
197 |
198 | });
199 |
200 | describe('Month boundaries restrictions', function() {
201 |
202 | beforeEach(function() {
203 | $scope.date = '2014-05-17';
204 | compile();
205 | });
206 |
207 | describe("when selectOtherMonths attribute has the 'next' value", function() {
208 |
209 | it("adds 'pickadate-enabled' class to next month dates", function() {
210 | expect($('li').last()).to.have.class('pickadate-enabled');
211 | });
212 |
213 | it("doesn't add 'pickadate-enabled' class to previous month dates", function() {
214 | expect($('li:contains(29)').first()).not.to.have.class('pickadate-enabled');
215 | });
216 |
217 | it("changes to the next month if a date from that other month is clicked", function() {
218 | browserTrigger($('li').last(), 'click');
219 | expect($('.pickadate-centered-heading')).to.have.text('June 2014');
220 | });
221 |
222 | describe("when the month is December", function() {
223 |
224 | beforeEach(function() {
225 | $scope.date = '2014-12-22';
226 | compile();
227 | });
228 |
229 | it("adds 'pickadate-enabled' class to next month/year dates", function() {
230 | expect($('li').last()).to.have.class('pickadate-enabled');
231 | });
232 |
233 | it("changes to January when a date from that month is clicked", function() {
234 | browserTrigger($('li').last(), 'click');
235 | expect($('.pickadate-centered-heading')).to.have.text('January 2015');
236 | });
237 | });
238 | });
239 |
240 | describe("when selectOtherMonths attribute has the 'previous' value", function() {
241 |
242 | var html = '';
243 |
244 | beforeEach(function() {
245 | compile(html);
246 | });
247 |
248 | it("doesn't add 'pickadate-enabled' class to next month dates", function() {
249 | expect($('li').last()).not.to.have.class('pickadate-enabled');
250 | });
251 |
252 | it("adds 'pickadate-enabled' class to previous month dates", function() {
253 | expect($('li:contains(29)').first()).to.have.class('pickadate-enabled');
254 | });
255 |
256 | it("changes to the previous month if a date from that other month is clicked", function() {
257 | browserTrigger($('li:contains(29)').first(), 'click');
258 | expect($('.pickadate-centered-heading')).to.have.text('April 2014');
259 | });
260 |
261 | describe("when the month is January", function() {
262 |
263 | beforeEach(function() {
264 | $scope.date = '2015-01-22';
265 | compile(html);
266 | });
267 |
268 | it("adds 'pickadate-enabled' class to previous month/year dates", function() {
269 | expect($('li:contains(29)').first()).to.have.class('pickadate-enabled');
270 | });
271 |
272 | it("changes to December when a date from that month is clicked", function() {
273 | browserTrigger($('li:contains(29)').first(), 'click');
274 | expect($('.pickadate-centered-heading')).to.have.text('December 2014');
275 | });
276 | });
277 | });
278 |
279 | describe("when selectOtherMonths attribute has the 'both' value", function() {
280 |
281 | var html = '';
282 |
283 | beforeEach(function() {
284 | compile(html);
285 | });
286 |
287 | it("adds 'pickadate-enabled' class to next month dates", function() {
288 | expect($('li').last()).to.have.class('pickadate-enabled');
289 | });
290 |
291 | it("adds 'pickadate-enabled' class to previous month dates", function() {
292 | expect($('li:contains(29)').first()).to.have.class('pickadate-enabled');
293 | });
294 |
295 | it("changes to the next month if a date from that other month is clicked", function() {
296 | browserTrigger($('li').last(), 'click');
297 | expect($('.pickadate-centered-heading')).to.have.text('June 2014');
298 | });
299 |
300 | it("changes to the previous month if a date from that other month is clicked", function() {
301 | browserTrigger($('li:contains(29)').first(), 'click');
302 | expect($('.pickadate-centered-heading')).to.have.text('April 2014');
303 | });
304 | });
305 |
306 | describe("when selectOtherMonths attribute has no value", function() {
307 |
308 | var html = '';
309 |
310 | it("both previous and next month dates are disabled", function() {
311 | compile(html);
312 | expect($('li').last()).not.to.have.class('pickadate-enabled');
313 | expect($('li:contains(29)').first()).not.to.have.class('pickadate-enabled');
314 | });
315 | });
316 | });
317 |
318 | describe('Month Navigation', function() {
319 |
320 | beforeEach(function() {
321 | $scope.date = '2014-05-17';
322 | });
323 |
324 | it("renders the current month", function(){
325 | compile();
326 | expect($('.pickadate-centered-heading')).to.have.text('May 2014');
327 | });
328 |
329 | it("changes to the previous month", function() {
330 | compile();
331 |
332 | browserTrigger($('.pickadate-prev'), 'click');
333 | expect($('.pickadate-centered-heading')).to.have.text('April 2014');
334 |
335 | browserTrigger($('.pickadate-prev'), 'click');
336 | expect($('.pickadate-centered-heading')).to.have.text('March 2014');
337 | });
338 |
339 | it("changes to the next month", function() {
340 | compile();
341 |
342 | browserTrigger($('.pickadate-next'), 'click');
343 | expect($('.pickadate-centered-heading')).to.have.text('June 2014');
344 |
345 | browserTrigger($('.pickadate-next'), 'click');
346 | expect($('.pickadate-centered-heading')).to.have.text('July 2014');
347 | });
348 |
349 | describe('Disabled months', function() {
350 |
351 | it("doesn't render the prev button if prev month < minDate", function() {
352 | $scope.minDate = '2014-04-04';
353 | compile();
354 |
355 | expect($('.pickadate-prev')).not.to.have.class('ng-hide');
356 |
357 | browserTrigger($('.pickadate-prev'), 'click');
358 | expect($('.pickadate-centered-heading')).to.have.text('April 2014');
359 |
360 | expect($('.pickadate-prev')).to.have.class('ng-hide');
361 | });
362 |
363 | it("doesn't render the next button if next month > maxDate", function() {
364 | $scope.maxDate = '2014-06-04';
365 | compile();
366 |
367 | expect($('.pickadate-next')).not.to.have.class('ng-hide');
368 |
369 | browserTrigger($('.pickadate-next'), 'click');
370 | expect($('.pickadate-centered-heading')).to.have.text('June 2014');
371 |
372 | expect($('.pickadate-next')).to.have.class('ng-hide');
373 | });
374 |
375 | it("renders the next button if maxDate is set to the beginning of a month", function() {
376 | $scope.date = '2014-08-31';
377 | $scope.maxDate = '2014-09-01';
378 | compile();
379 |
380 | expect($('.pickadate-next')).not.to.have.class('ng-hide');
381 | });
382 |
383 | });
384 | });
385 |
386 | describe('Configure the first day of the week', function() {
387 | var defaultDay;
388 |
389 | var firstCalendarDay = function(weekStartsOn) {
390 | $scope.weekStartsOn = weekStartsOn;
391 | compile();
392 | return $('ul:last-child li:first-child').text();
393 | };
394 |
395 | beforeEach(function() {
396 | defaultDay = firstCalendarDay(0);
397 | });
398 |
399 | it('changes the first day of the week', function() {
400 | for (var weekStartsOn = 1; weekStartsOn < 7; weekStartsOn++) {
401 | expect(firstCalendarDay(weekStartsOn)).to.not.equal(defaultDay);
402 | }
403 | });
404 |
405 | it('sets weekStartsOn to 0 if it is invalid', function() {
406 | angular.forEach([7, -1, 'foo'], function(weekStartsOn) {
407 | expect(firstCalendarDay(weekStartsOn)).to.equal(defaultDay);
408 | });
409 | });
410 |
411 | });
412 |
413 | describe('Default date', function() {
414 | beforeEach(function() {
415 | this.clock = sinon.useFakeTimers(1431025777408);
416 | });
417 |
418 | afterEach(function() {
419 | this.clock.restore();
420 | });
421 |
422 | it("renders the specified yearMonth by default if no date is selected", function() {
423 | $scope.defaultDate = '2014-11-10';
424 | compile();
425 |
426 | expect($('.pickadate-centered-heading')).to.have.text('November 2014');
427 | });
428 |
429 | it("renders the specified yearMonth by default even if a date is selected", function() {
430 | $scope.defaultDate = '2014-11-10';
431 | $scope.date = '2014-03-01';
432 | compile();
433 |
434 | expect($('.pickadate-centered-heading')).to.have.text('November 2014');
435 | });
436 |
437 | it("renders the current month if no date is selected and no default date is specified ", function() {
438 | compile();
439 |
440 | expect($('.pickadate-centered-heading')).to.have.text('May 2015');
441 | });
442 |
443 | it("renders the selected date month if no default date is specified", function() {
444 | $scope.date = '2014-08-03';
445 | compile();
446 |
447 | expect($('.pickadate-centered-heading')).to.have.text('August 2014');
448 | });
449 |
450 | });
451 |
452 | describe('Translations', function() {
453 |
454 | it('uses the default translations if not translations are specified', function() {
455 | compile();
456 | expect($('.pickadate-prev')).to.have.text('prev');
457 | expect($('.pickadate-next')).to.have.text('next');
458 | });
459 |
460 | it('uses the translations previously set in the pickadateI18nProvider', function() {
461 | pickadateI18nProvider.translations = {
462 | prev: 'ant',
463 | next: 'sig'
464 | };
465 | compile();
466 | expect($('.pickadate-prev')).to.have.text('ant');
467 | expect($('.pickadate-next')).to.have.text('sig');
468 | });
469 |
470 | it('accepts valid html translations', function() {
471 | pickadateI18nProvider.translations = {
472 | prev: ' ant',
473 | next: 'sig '
474 | };
475 | compile();
476 | expect($('.pickadate-prev')).to.have.html(' ant');
477 | expect($('.pickadate-next')).to.have.html('sig ');
478 | });
479 |
480 | });
481 |
482 | describe('Using only required scope properties', function() {
483 |
484 | it("doesn't throw an error if only the required scope properties are being binded", function() {
485 | expect(function(){
486 | compile('');
487 | }).not.to.throw();
488 |
489 | });
490 |
491 | });
492 |
493 | describe('Date formats', function() {
494 |
495 | beforeEach(function() {
496 | this.clock = sinon.useFakeTimers(1431025777408);
497 | });
498 |
499 | afterEach(function() {
500 | this.clock.restore();
501 | });
502 |
503 | var compileFormat = function(format) {
504 | compile('');
505 | };
506 |
507 | it("sets the date in the right format after selecting it", function() {
508 | compileFormat('dd/MM/yyyy');
509 | browserTrigger($('.pickadate-enabled:first'), 'click');
510 | expect($scope.date).to.equal('01/05/2015');
511 | });
512 |
513 | it("takes the initial date in the right format", function() {
514 | $scope.date = '20/02/2012';
515 | compileFormat('dd/mm/yyyy');
516 |
517 | expect($scope.date).to.equal('20/02/2012');
518 | expect($('.pickadate-centered-heading')).to.have.text('February 2012');
519 | expect($('li:contains(20)')).to.have.class('pickadate-active');
520 | });
521 |
522 | });
523 |
524 | describe('Multiple dates', function() {
525 |
526 | beforeEach(function() {
527 | this.clock = sinon.useFakeTimers(1431025777408);
528 | });
529 |
530 | afterEach(function() {
531 | this.clock.restore();
532 | });
533 |
534 | describe('When ng-model is initially empty', function() {
535 |
536 | beforeEach(function() {
537 | compile('');
538 | });
539 |
540 | it('sets the current ngModel to an empty array if its undefined', function() {
541 | expect($scope.date).to.be.empty;
542 | });
543 |
544 | it('adds the selected date to the ngModel array', function() {
545 | browserTrigger($('.pickadate-enabled:contains(20)'), 'click');
546 | browserTrigger($('.pickadate-enabled:contains(22)'), 'click');
547 | expect($scope.date).to.deep.equal(['2015-05-20', '2015-05-22']);
548 | });
549 |
550 | it('removes the selected date of the ngModel array if it was previously selected', function() {
551 | browserTrigger($('.pickadate-enabled:contains(7)'), 'click');
552 | browserTrigger($('.pickadate-enabled:contains(15)'), 'click');
553 | expect($scope.date).to.deep.equal(['2015-05-07', '2015-05-15']);
554 |
555 | browserTrigger($('.pickadate-enabled:contains(7)'), 'click');
556 | expect($scope.date).to.deep.equal(['2015-05-15']);
557 | });
558 | })
559 |
560 | describe('When an ng-model was already set', function() {
561 |
562 | beforeEach(function() {
563 | $scope.date = ['2015-05-07', '2015-05-10', '2015-05-13'];
564 | compile('');
565 | })
566 |
567 | it('removes the selected date of the ngModel array if it was previously selected', function() {
568 | browserTrigger($('.pickadate-enabled:contains(7)'), 'click');
569 | expect($scope.date).to.deep.equal(['2015-05-10', '2015-05-13'])
570 | });
571 |
572 | })
573 |
574 | describe('Rendering', function() {
575 |
576 | beforeEach(function() {
577 | compile('');
578 | });
579 |
580 | it('renders the multiple dates', function() {
581 | $scope.date = ['2015-05-07', '2015-05-10', '2015-05-13'];
582 | $scope.$digest();
583 |
584 | expect($('.pickadate-active').get()).to.have.length(3);
585 | expect($('.pickadate-active:eq(0)')).to.have.text('7');
586 | expect($('.pickadate-active:eq(1)')).to.have.text('10');
587 | expect($('.pickadate-active:eq(2)')).to.have.text('13');
588 | });
589 |
590 | });
591 |
592 | describe('Disabled dates', function() {
593 |
594 | beforeEach(function() {
595 | compile('');
596 | });
597 |
598 | it("removes the disabled dates from the initial date array", function() {
599 | $scope.date = ['2015-05-07', '2015-05-10', '2015-05-13'];
600 | $scope.disabledDates = ['2015-05-10'];
601 | $scope.$digest();
602 |
603 | expect($('.pickadate-active').get()).to.have.length(2);
604 | expect($('.pickadate-active:eq(0)')).to.have.text('7');
605 | expect($('.pickadate-active:eq(1)')).to.have.text('13');
606 | });
607 |
608 | it("removes the disabled date if the ngModel is updated and contains disabled dates", function() {
609 | $scope.disabledDates = ['2015-05-10', '2015-05-13'];
610 | $scope.$digest();
611 |
612 | $scope.date = ['2015-05-07', '2015-05-10', '2015-05-13'];
613 | $scope.$digest();
614 |
615 | expect($('.pickadate-active').get()).to.have.length(1);
616 | expect($('.pickadate-active:eq(0)')).to.have.text('7');
617 | });
618 |
619 | });
620 |
621 | describe("Don't allow blank date", function() {
622 | beforeEach(function() {
623 | compile('');
624 | });
625 |
626 | it("doesn't remove the last selected date", function() {
627 | $scope.date = ['2015-05-25'];
628 | $scope.$digest();
629 | browserTrigger($('.pickadate-enabled:contains(25)'), 'click');
630 | expect($scope.date).to.deep.equal(['2015-05-25']);
631 | });
632 |
633 | it("removes the selected date when it is not the last one", function() {
634 | $scope.date = ['2015-05-25', '2015-05-20'];
635 | $scope.$digest();
636 | browserTrigger($('.pickadate-enabled:contains(25)'), 'click');
637 | expect($scope.date).to.deep.equal(['2015-05-20']);
638 | });
639 | });
640 |
641 | describe("Allow blank date", function() {
642 | beforeEach(function() {
643 | compile('');
644 | });
645 |
646 | it("removes the last selected date", function() {
647 | $scope.date = ['2015-05-25'];
648 | $scope.$digest();
649 | browserTrigger($('.pickadate-enabled:contains(25)'), 'click');
650 | expect($scope.date).to.be.empty;
651 | });
652 | });
653 |
654 | });
655 |
656 | describe('When used as a modal', function() {
657 |
658 | var inputHtml = '',
661 | input, form;
662 |
663 | beforeEach(function() {
664 | $scope.date = '2014-05-17';
665 | $scope.minDate = '2014-01-01';
666 | compile(inputHtml);
667 | form = element;
668 | input = $('input');
669 | element = $('.pickadate');
670 | });
671 |
672 | it('adds the pickadate-modal class', function() {
673 | expect(element).to.have.class('pickadate-modal');
674 | });
675 |
676 | it('renders the datepicker already hidden', function() {
677 | expect(element).to.have.class('ng-hide');
678 | });
679 |
680 | it('displays the datepicker when the input is focused', function() {
681 | browserTrigger(input, 'focus');
682 | expect(element).not.to.have.class('ng-hide');
683 | });
684 |
685 | it('hides the datepicker when the user clicks outside the datepicker', function() {
686 | expect(element).to.have.class('ng-hide');
687 |
688 | browserTrigger(input, 'focus');
689 | expect(element).not.to.have.class('ng-hide');
690 |
691 | browserTrigger(document.body, 'click');
692 | expect(element).to.have.class('ng-hide');
693 | });
694 |
695 | it("doesn't hide the datepicker if the calendar is clicked", function() {
696 | expect(element).to.have.class('ng-hide');
697 |
698 | browserTrigger(input, 'focus');
699 | expect(element).not.to.have.class('ng-hide');
700 |
701 | browserTrigger($('.pickadate-centered-heading'), 'click');
702 | expect(element).not.to.have.class('ng-hide');
703 | });
704 |
705 | it("hides the datepicker if a date is selected", function() {
706 | expect(element).to.have.class('ng-hide');
707 |
708 | browserTrigger(input, 'focus');
709 | expect(element).not.to.have.class('ng-hide');
710 |
711 | browserTrigger($('.pickadate-enabled:first'), 'click');
712 | expect(element).to.have.class('ng-hide');
713 | });
714 |
715 | it('sets the input value with the ng-model value', function() {
716 | expect(input).to.have.value('2014-05-17');
717 |
718 | browserTrigger(input, 'focus');
719 | browserTrigger($('.pickadate-enabled:first'), 'click');
720 |
721 | expect(input).to.have.value('2014-05-01');
722 |
723 | browserTrigger(input, 'focus');
724 | browserTrigger($('.pickadate-enabled:last'), 'click');
725 |
726 | expect(input).to.have.value('2014-05-31');
727 | });
728 |
729 | describe('and the input value is manually changed', function() {
730 |
731 | it('updates the ng-model with the entered value', function() {
732 | expect($scope.date).to.eq('2014-05-17');
733 |
734 | input.val('2015-02-20');
735 | browserTrigger(input, 'change');
736 |
737 | expect($scope.date).to.eq('2015-02-20');
738 | });
739 |
740 | it('sets the ng-model to undefined if the date is not in a valid range', function() {
741 | expect($scope.date).to.eq('2014-05-17');
742 | expect($scope.dateForm.$error).not.to.have.property('date');
743 |
744 | input.val('2012-02-20');
745 | browserTrigger(input, 'change');
746 |
747 | expect($scope.dateForm.$error).to.have.property('date');
748 | });
749 |
750 | it('sets the ng-model to undefined if the date is not valid', function() {
751 | expect($scope.date).to.eq('2014-05-17');
752 | expect($scope.dateForm.$error).not.to.have.property('date');
753 |
754 | input.val('2012-02-');
755 | browserTrigger(input, 'change');
756 |
757 | expect($scope.dateForm.$error).to.have.property('date');
758 | });
759 |
760 | });
761 |
762 | describe('and the input has already a value', function() {
763 |
764 | function compileModal(value) {
765 | var html =
766 | '';
769 |
770 | $scope.minDate = '2014-01-01';
771 | compile(html);
772 |
773 | form = element;
774 | input = $('input');
775 | element = $('.pickadate');
776 | }
777 |
778 | it("sets the ng-model value as the element's value", function() {
779 | $scope.ngModelDate = '2014-05-10';
780 | compileModal('2015-01-14');
781 |
782 | expect($scope.ngModelDate).to.eq('2015-01-14');
783 | });
784 |
785 | it("sets the ng-model value as undefined if is not in a valid range", function() {
786 | compileModal('2011-01-14');
787 | expect($scope.ngModelDate).to.be.undefined;
788 | });
789 |
790 | });
791 |
792 | });
793 |
794 | });
795 |
--------------------------------------------------------------------------------
/test/lib/angular-mocks-1.2.21.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.2.21
3 | * (c) 2010-2014 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular, undefined) {
7 |
8 | 'use strict';
9 |
10 | /**
11 | * @ngdoc object
12 | * @name angular.mock
13 | * @description
14 | *
15 | * Namespace from 'angular-mocks.js' which contains testing related code.
16 | */
17 | angular.mock = {};
18 |
19 | /**
20 | * ! This is a private undocumented service !
21 | *
22 | * @name $browser
23 | *
24 | * @description
25 | * This service is a mock implementation of {@link ng.$browser}. It provides fake
26 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr,
27 | * cookies, etc...
28 | *
29 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except
30 | * that there are several helper methods available which can be used in tests.
31 | */
32 | angular.mock.$BrowserProvider = function() {
33 | this.$get = function() {
34 | return new angular.mock.$Browser();
35 | };
36 | };
37 |
38 | angular.mock.$Browser = function() {
39 | var self = this;
40 |
41 | this.isMock = true;
42 | self.$$url = "http://server/";
43 | self.$$lastUrl = self.$$url; // used by url polling fn
44 | self.pollFns = [];
45 |
46 | // TODO(vojta): remove this temporary api
47 | self.$$completeOutstandingRequest = angular.noop;
48 | self.$$incOutstandingRequestCount = angular.noop;
49 |
50 |
51 | // register url polling fn
52 |
53 | self.onUrlChange = function(listener) {
54 | self.pollFns.push(
55 | function() {
56 | if (self.$$lastUrl != self.$$url) {
57 | self.$$lastUrl = self.$$url;
58 | listener(self.$$url);
59 | }
60 | }
61 | );
62 |
63 | return listener;
64 | };
65 |
66 | self.cookieHash = {};
67 | self.lastCookieHash = {};
68 | self.deferredFns = [];
69 | self.deferredNextId = 0;
70 |
71 | self.defer = function(fn, delay) {
72 | delay = delay || 0;
73 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
74 | self.deferredFns.sort(function(a,b){ return a.time - b.time;});
75 | return self.deferredNextId++;
76 | };
77 |
78 |
79 | /**
80 | * @name $browser#defer.now
81 | *
82 | * @description
83 | * Current milliseconds mock time.
84 | */
85 | self.defer.now = 0;
86 |
87 |
88 | self.defer.cancel = function(deferId) {
89 | var fnIndex;
90 |
91 | angular.forEach(self.deferredFns, function(fn, index) {
92 | if (fn.id === deferId) fnIndex = index;
93 | });
94 |
95 | if (fnIndex !== undefined) {
96 | self.deferredFns.splice(fnIndex, 1);
97 | return true;
98 | }
99 |
100 | return false;
101 | };
102 |
103 |
104 | /**
105 | * @name $browser#defer.flush
106 | *
107 | * @description
108 | * Flushes all pending requests and executes the defer callbacks.
109 | *
110 | * @param {number=} number of milliseconds to flush. See {@link #defer.now}
111 | */
112 | self.defer.flush = function(delay) {
113 | if (angular.isDefined(delay)) {
114 | self.defer.now += delay;
115 | } else {
116 | if (self.deferredFns.length) {
117 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
118 | } else {
119 | throw new Error('No deferred tasks to be flushed');
120 | }
121 | }
122 |
123 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
124 | self.deferredFns.shift().fn();
125 | }
126 | };
127 |
128 | self.$$baseHref = '';
129 | self.baseHref = function() {
130 | return this.$$baseHref;
131 | };
132 | };
133 | angular.mock.$Browser.prototype = {
134 |
135 | /**
136 | * @name $browser#poll
137 | *
138 | * @description
139 | * run all fns in pollFns
140 | */
141 | poll: function poll() {
142 | angular.forEach(this.pollFns, function(pollFn){
143 | pollFn();
144 | });
145 | },
146 |
147 | addPollFn: function(pollFn) {
148 | this.pollFns.push(pollFn);
149 | return pollFn;
150 | },
151 |
152 | url: function(url, replace) {
153 | if (url) {
154 | this.$$url = url;
155 | return this;
156 | }
157 |
158 | return this.$$url;
159 | },
160 |
161 | cookies: function(name, value) {
162 | if (name) {
163 | if (angular.isUndefined(value)) {
164 | delete this.cookieHash[name];
165 | } else {
166 | if (angular.isString(value) && //strings only
167 | value.length <= 4096) { //strict cookie storage limits
168 | this.cookieHash[name] = value;
169 | }
170 | }
171 | } else {
172 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) {
173 | this.lastCookieHash = angular.copy(this.cookieHash);
174 | this.cookieHash = angular.copy(this.cookieHash);
175 | }
176 | return this.cookieHash;
177 | }
178 | },
179 |
180 | notifyWhenNoOutstandingRequests: function(fn) {
181 | fn();
182 | }
183 | };
184 |
185 |
186 | /**
187 | * @ngdoc provider
188 | * @name $exceptionHandlerProvider
189 | *
190 | * @description
191 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
192 | * passed into the `$exceptionHandler`.
193 | */
194 |
195 | /**
196 | * @ngdoc service
197 | * @name $exceptionHandler
198 | *
199 | * @description
200 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
201 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
202 | * information.
203 | *
204 | *
205 | * ```js
206 | * describe('$exceptionHandlerProvider', function() {
207 | *
208 | * it('should capture log messages and exceptions', function() {
209 | *
210 | * module(function($exceptionHandlerProvider) {
211 | * $exceptionHandlerProvider.mode('log');
212 | * });
213 | *
214 | * inject(function($log, $exceptionHandler, $timeout) {
215 | * $timeout(function() { $log.log(1); });
216 | * $timeout(function() { $log.log(2); throw 'banana peel'; });
217 | * $timeout(function() { $log.log(3); });
218 | * expect($exceptionHandler.errors).toEqual([]);
219 | * expect($log.assertEmpty());
220 | * $timeout.flush();
221 | * expect($exceptionHandler.errors).toEqual(['banana peel']);
222 | * expect($log.log.logs).toEqual([[1], [2], [3]]);
223 | * });
224 | * });
225 | * });
226 | * ```
227 | */
228 |
229 | angular.mock.$ExceptionHandlerProvider = function() {
230 | var handler;
231 |
232 | /**
233 | * @ngdoc method
234 | * @name $exceptionHandlerProvider#mode
235 | *
236 | * @description
237 | * Sets the logging mode.
238 | *
239 | * @param {string} mode Mode of operation, defaults to `rethrow`.
240 | *
241 | * - `rethrow`: If any errors are passed into the handler in tests, it typically
242 | * means that there is a bug in the application or test, so this mock will
243 | * make these tests fail.
244 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
245 | * mode stores an array of errors in `$exceptionHandler.errors`, to allow later
246 | * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
247 | * {@link ngMock.$log#reset reset()}
248 | */
249 | this.mode = function(mode) {
250 | switch(mode) {
251 | case 'rethrow':
252 | handler = function(e) {
253 | throw e;
254 | };
255 | break;
256 | case 'log':
257 | var errors = [];
258 |
259 | handler = function(e) {
260 | if (arguments.length == 1) {
261 | errors.push(e);
262 | } else {
263 | errors.push([].slice.call(arguments, 0));
264 | }
265 | };
266 |
267 | handler.errors = errors;
268 | break;
269 | default:
270 | throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
271 | }
272 | };
273 |
274 | this.$get = function() {
275 | return handler;
276 | };
277 |
278 | this.mode('rethrow');
279 | };
280 |
281 |
282 | /**
283 | * @ngdoc service
284 | * @name $log
285 | *
286 | * @description
287 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays
288 | * (one array per logging level). These arrays are exposed as `logs` property of each of the
289 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`.
290 | *
291 | */
292 | angular.mock.$LogProvider = function() {
293 | var debug = true;
294 |
295 | function concat(array1, array2, index) {
296 | return array1.concat(Array.prototype.slice.call(array2, index));
297 | }
298 |
299 | this.debugEnabled = function(flag) {
300 | if (angular.isDefined(flag)) {
301 | debug = flag;
302 | return this;
303 | } else {
304 | return debug;
305 | }
306 | };
307 |
308 | this.$get = function () {
309 | var $log = {
310 | log: function() { $log.log.logs.push(concat([], arguments, 0)); },
311 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
312 | info: function() { $log.info.logs.push(concat([], arguments, 0)); },
313 | error: function() { $log.error.logs.push(concat([], arguments, 0)); },
314 | debug: function() {
315 | if (debug) {
316 | $log.debug.logs.push(concat([], arguments, 0));
317 | }
318 | }
319 | };
320 |
321 | /**
322 | * @ngdoc method
323 | * @name $log#reset
324 | *
325 | * @description
326 | * Reset all of the logging arrays to empty.
327 | */
328 | $log.reset = function () {
329 | /**
330 | * @ngdoc property
331 | * @name $log#log.logs
332 | *
333 | * @description
334 | * Array of messages logged using {@link ngMock.$log#log}.
335 | *
336 | * @example
337 | * ```js
338 | * $log.log('Some Log');
339 | * var first = $log.log.logs.unshift();
340 | * ```
341 | */
342 | $log.log.logs = [];
343 | /**
344 | * @ngdoc property
345 | * @name $log#info.logs
346 | *
347 | * @description
348 | * Array of messages logged using {@link ngMock.$log#info}.
349 | *
350 | * @example
351 | * ```js
352 | * $log.info('Some Info');
353 | * var first = $log.info.logs.unshift();
354 | * ```
355 | */
356 | $log.info.logs = [];
357 | /**
358 | * @ngdoc property
359 | * @name $log#warn.logs
360 | *
361 | * @description
362 | * Array of messages logged using {@link ngMock.$log#warn}.
363 | *
364 | * @example
365 | * ```js
366 | * $log.warn('Some Warning');
367 | * var first = $log.warn.logs.unshift();
368 | * ```
369 | */
370 | $log.warn.logs = [];
371 | /**
372 | * @ngdoc property
373 | * @name $log#error.logs
374 | *
375 | * @description
376 | * Array of messages logged using {@link ngMock.$log#error}.
377 | *
378 | * @example
379 | * ```js
380 | * $log.error('Some Error');
381 | * var first = $log.error.logs.unshift();
382 | * ```
383 | */
384 | $log.error.logs = [];
385 | /**
386 | * @ngdoc property
387 | * @name $log#debug.logs
388 | *
389 | * @description
390 | * Array of messages logged using {@link ngMock.$log#debug}.
391 | *
392 | * @example
393 | * ```js
394 | * $log.debug('Some Error');
395 | * var first = $log.debug.logs.unshift();
396 | * ```
397 | */
398 | $log.debug.logs = [];
399 | };
400 |
401 | /**
402 | * @ngdoc method
403 | * @name $log#assertEmpty
404 | *
405 | * @description
406 | * Assert that the all of the logging methods have no logged messages. If messages present, an
407 | * exception is thrown.
408 | */
409 | $log.assertEmpty = function() {
410 | var errors = [];
411 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
412 | angular.forEach($log[logLevel].logs, function(log) {
413 | angular.forEach(log, function (logItem) {
414 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
415 | (logItem.stack || ''));
416 | });
417 | });
418 | });
419 | if (errors.length) {
420 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
421 | "an expected log message was not checked and removed:");
422 | errors.push('');
423 | throw new Error(errors.join('\n---------\n'));
424 | }
425 | };
426 |
427 | $log.reset();
428 | return $log;
429 | };
430 | };
431 |
432 |
433 | /**
434 | * @ngdoc service
435 | * @name $interval
436 | *
437 | * @description
438 | * Mock implementation of the $interval service.
439 | *
440 | * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
441 | * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
442 | * time.
443 | *
444 | * @param {function()} fn A function that should be called repeatedly.
445 | * @param {number} delay Number of milliseconds between each function call.
446 | * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
447 | * indefinitely.
448 | * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
449 | * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
450 | * @returns {promise} A promise which will be notified on each iteration.
451 | */
452 | angular.mock.$IntervalProvider = function() {
453 | this.$get = ['$rootScope', '$q',
454 | function($rootScope, $q) {
455 | var repeatFns = [],
456 | nextRepeatId = 0,
457 | now = 0;
458 |
459 | var $interval = function(fn, delay, count, invokeApply) {
460 | var deferred = $q.defer(),
461 | promise = deferred.promise,
462 | iteration = 0,
463 | skipApply = (angular.isDefined(invokeApply) && !invokeApply);
464 |
465 | count = (angular.isDefined(count)) ? count : 0;
466 | promise.then(null, null, fn);
467 |
468 | promise.$$intervalId = nextRepeatId;
469 |
470 | function tick() {
471 | deferred.notify(iteration++);
472 |
473 | if (count > 0 && iteration >= count) {
474 | var fnIndex;
475 | deferred.resolve(iteration);
476 |
477 | angular.forEach(repeatFns, function(fn, index) {
478 | if (fn.id === promise.$$intervalId) fnIndex = index;
479 | });
480 |
481 | if (fnIndex !== undefined) {
482 | repeatFns.splice(fnIndex, 1);
483 | }
484 | }
485 |
486 | if (!skipApply) $rootScope.$apply();
487 | }
488 |
489 | repeatFns.push({
490 | nextTime:(now + delay),
491 | delay: delay,
492 | fn: tick,
493 | id: nextRepeatId,
494 | deferred: deferred
495 | });
496 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
497 |
498 | nextRepeatId++;
499 | return promise;
500 | };
501 | /**
502 | * @ngdoc method
503 | * @name $interval#cancel
504 | *
505 | * @description
506 | * Cancels a task associated with the `promise`.
507 | *
508 | * @param {promise} promise A promise from calling the `$interval` function.
509 | * @returns {boolean} Returns `true` if the task was successfully cancelled.
510 | */
511 | $interval.cancel = function(promise) {
512 | if(!promise) return false;
513 | var fnIndex;
514 |
515 | angular.forEach(repeatFns, function(fn, index) {
516 | if (fn.id === promise.$$intervalId) fnIndex = index;
517 | });
518 |
519 | if (fnIndex !== undefined) {
520 | repeatFns[fnIndex].deferred.reject('canceled');
521 | repeatFns.splice(fnIndex, 1);
522 | return true;
523 | }
524 |
525 | return false;
526 | };
527 |
528 | /**
529 | * @ngdoc method
530 | * @name $interval#flush
531 | * @description
532 | *
533 | * Runs interval tasks scheduled to be run in the next `millis` milliseconds.
534 | *
535 | * @param {number=} millis maximum timeout amount to flush up until.
536 | *
537 | * @return {number} The amount of time moved forward.
538 | */
539 | $interval.flush = function(millis) {
540 | now += millis;
541 | while (repeatFns.length && repeatFns[0].nextTime <= now) {
542 | var task = repeatFns[0];
543 | task.fn();
544 | task.nextTime += task.delay;
545 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
546 | }
547 | return millis;
548 | };
549 |
550 | return $interval;
551 | }];
552 | };
553 |
554 |
555 | /* jshint -W101 */
556 | /* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
557 | * This directive should go inside the anonymous function but a bug in JSHint means that it would
558 | * not be enacted early enough to prevent the warning.
559 | */
560 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
561 |
562 | function jsonStringToDate(string) {
563 | var match;
564 | if (match = string.match(R_ISO8061_STR)) {
565 | var date = new Date(0),
566 | tzHour = 0,
567 | tzMin = 0;
568 | if (match[9]) {
569 | tzHour = int(match[9] + match[10]);
570 | tzMin = int(match[9] + match[11]);
571 | }
572 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
573 | date.setUTCHours(int(match[4]||0) - tzHour,
574 | int(match[5]||0) - tzMin,
575 | int(match[6]||0),
576 | int(match[7]||0));
577 | return date;
578 | }
579 | return string;
580 | }
581 |
582 | function int(str) {
583 | return parseInt(str, 10);
584 | }
585 |
586 | function padNumber(num, digits, trim) {
587 | var neg = '';
588 | if (num < 0) {
589 | neg = '-';
590 | num = -num;
591 | }
592 | num = '' + num;
593 | while(num.length < digits) num = '0' + num;
594 | if (trim)
595 | num = num.substr(num.length - digits);
596 | return neg + num;
597 | }
598 |
599 |
600 | /**
601 | * @ngdoc type
602 | * @name angular.mock.TzDate
603 | * @description
604 | *
605 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
606 | *
607 | * Mock of the Date type which has its timezone specified via constructor arg.
608 | *
609 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone
610 | * offset, so that we can test code that depends on local timezone settings without dependency on
611 | * the time zone settings of the machine where the code is running.
612 | *
613 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
614 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
615 | *
616 | * @example
617 | * !!!! WARNING !!!!!
618 | * This is not a complete Date object so only methods that were implemented can be called safely.
619 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype.
620 | *
621 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
622 | * incomplete we might be missing some non-standard methods. This can result in errors like:
623 | * "Date.prototype.foo called on incompatible Object".
624 | *
625 | * ```js
626 | * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
627 | * newYearInBratislava.getTimezoneOffset() => -60;
628 | * newYearInBratislava.getFullYear() => 2010;
629 | * newYearInBratislava.getMonth() => 0;
630 | * newYearInBratislava.getDate() => 1;
631 | * newYearInBratislava.getHours() => 0;
632 | * newYearInBratislava.getMinutes() => 0;
633 | * newYearInBratislava.getSeconds() => 0;
634 | * ```
635 | *
636 | */
637 | angular.mock.TzDate = function (offset, timestamp) {
638 | var self = new Date(0);
639 | if (angular.isString(timestamp)) {
640 | var tsStr = timestamp;
641 |
642 | self.origDate = jsonStringToDate(timestamp);
643 |
644 | timestamp = self.origDate.getTime();
645 | if (isNaN(timestamp))
646 | throw {
647 | name: "Illegal Argument",
648 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
649 | };
650 | } else {
651 | self.origDate = new Date(timestamp);
652 | }
653 |
654 | var localOffset = new Date(timestamp).getTimezoneOffset();
655 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
656 | self.date = new Date(timestamp + self.offsetDiff);
657 |
658 | self.getTime = function() {
659 | return self.date.getTime() - self.offsetDiff;
660 | };
661 |
662 | self.toLocaleDateString = function() {
663 | return self.date.toLocaleDateString();
664 | };
665 |
666 | self.getFullYear = function() {
667 | return self.date.getFullYear();
668 | };
669 |
670 | self.getMonth = function() {
671 | return self.date.getMonth();
672 | };
673 |
674 | self.getDate = function() {
675 | return self.date.getDate();
676 | };
677 |
678 | self.getHours = function() {
679 | return self.date.getHours();
680 | };
681 |
682 | self.getMinutes = function() {
683 | return self.date.getMinutes();
684 | };
685 |
686 | self.getSeconds = function() {
687 | return self.date.getSeconds();
688 | };
689 |
690 | self.getMilliseconds = function() {
691 | return self.date.getMilliseconds();
692 | };
693 |
694 | self.getTimezoneOffset = function() {
695 | return offset * 60;
696 | };
697 |
698 | self.getUTCFullYear = function() {
699 | return self.origDate.getUTCFullYear();
700 | };
701 |
702 | self.getUTCMonth = function() {
703 | return self.origDate.getUTCMonth();
704 | };
705 |
706 | self.getUTCDate = function() {
707 | return self.origDate.getUTCDate();
708 | };
709 |
710 | self.getUTCHours = function() {
711 | return self.origDate.getUTCHours();
712 | };
713 |
714 | self.getUTCMinutes = function() {
715 | return self.origDate.getUTCMinutes();
716 | };
717 |
718 | self.getUTCSeconds = function() {
719 | return self.origDate.getUTCSeconds();
720 | };
721 |
722 | self.getUTCMilliseconds = function() {
723 | return self.origDate.getUTCMilliseconds();
724 | };
725 |
726 | self.getDay = function() {
727 | return self.date.getDay();
728 | };
729 |
730 | // provide this method only on browsers that already have it
731 | if (self.toISOString) {
732 | self.toISOString = function() {
733 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' +
734 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' +
735 | padNumber(self.origDate.getUTCDate(), 2) + 'T' +
736 | padNumber(self.origDate.getUTCHours(), 2) + ':' +
737 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' +
738 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' +
739 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z';
740 | };
741 | }
742 |
743 | //hide all methods not implemented in this mock that the Date prototype exposes
744 | var unimplementedMethods = ['getUTCDay',
745 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
746 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
747 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
748 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString',
749 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
750 |
751 | angular.forEach(unimplementedMethods, function(methodName) {
752 | self[methodName] = function() {
753 | throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
754 | };
755 | });
756 |
757 | return self;
758 | };
759 |
760 | //make "tzDateInstance instanceof Date" return true
761 | angular.mock.TzDate.prototype = Date.prototype;
762 | /* jshint +W101 */
763 |
764 | angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
765 |
766 | .config(['$provide', function($provide) {
767 |
768 | var reflowQueue = [];
769 | $provide.value('$$animateReflow', function(fn) {
770 | var index = reflowQueue.length;
771 | reflowQueue.push(fn);
772 | return function cancel() {
773 | reflowQueue.splice(index, 1);
774 | };
775 | });
776 |
777 | $provide.decorator('$animate', function($delegate, $$asyncCallback) {
778 | var animate = {
779 | queue : [],
780 | enabled : $delegate.enabled,
781 | triggerCallbacks : function() {
782 | $$asyncCallback.flush();
783 | },
784 | triggerReflow : function() {
785 | angular.forEach(reflowQueue, function(fn) {
786 | fn();
787 | });
788 | reflowQueue = [];
789 | }
790 | };
791 |
792 | angular.forEach(
793 | ['enter','leave','move','addClass','removeClass','setClass'], function(method) {
794 | animate[method] = function() {
795 | animate.queue.push({
796 | event : method,
797 | element : arguments[0],
798 | args : arguments
799 | });
800 | $delegate[method].apply($delegate, arguments);
801 | };
802 | });
803 |
804 | return animate;
805 | });
806 |
807 | }]);
808 |
809 |
810 | /**
811 | * @ngdoc function
812 | * @name angular.mock.dump
813 | * @description
814 | *
815 | * *NOTE*: this is not an injectable instance, just a globally available function.
816 | *
817 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for
818 | * debugging.
819 | *
820 | * This method is also available on window, where it can be used to display objects on debug
821 | * console.
822 | *
823 | * @param {*} object - any object to turn into string.
824 | * @return {string} a serialized string of the argument
825 | */
826 | angular.mock.dump = function(object) {
827 | return serialize(object);
828 |
829 | function serialize(object) {
830 | var out;
831 |
832 | if (angular.isElement(object)) {
833 | object = angular.element(object);
834 | out = angular.element('');
835 | angular.forEach(object, function(element) {
836 | out.append(angular.element(element).clone());
837 | });
838 | out = out.html();
839 | } else if (angular.isArray(object)) {
840 | out = [];
841 | angular.forEach(object, function(o) {
842 | out.push(serialize(o));
843 | });
844 | out = '[ ' + out.join(', ') + ' ]';
845 | } else if (angular.isObject(object)) {
846 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
847 | out = serializeScope(object);
848 | } else if (object instanceof Error) {
849 | out = object.stack || ('' + object.name + ': ' + object.message);
850 | } else {
851 | // TODO(i): this prevents methods being logged,
852 | // we should have a better way to serialize objects
853 | out = angular.toJson(object, true);
854 | }
855 | } else {
856 | out = String(object);
857 | }
858 |
859 | return out;
860 | }
861 |
862 | function serializeScope(scope, offset) {
863 | offset = offset || ' ';
864 | var log = [offset + 'Scope(' + scope.$id + '): {'];
865 | for ( var key in scope ) {
866 | if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
867 | log.push(' ' + key + ': ' + angular.toJson(scope[key]));
868 | }
869 | }
870 | var child = scope.$$childHead;
871 | while(child) {
872 | log.push(serializeScope(child, offset + ' '));
873 | child = child.$$nextSibling;
874 | }
875 | log.push('}');
876 | return log.join('\n' + offset);
877 | }
878 | };
879 |
880 | /**
881 | * @ngdoc service
882 | * @name $httpBackend
883 | * @description
884 | * Fake HTTP backend implementation suitable for unit testing applications that use the
885 | * {@link ng.$http $http service}.
886 | *
887 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less
888 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
889 | *
890 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so
891 | * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
892 | * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is
893 | * to verify whether a certain request has been sent or not, or alternatively just let the
894 | * application make requests, respond with pre-trained responses and assert that the end result is
895 | * what we expect it to be.
896 | *
897 | * This mock implementation can be used to respond with static or dynamic responses via the
898 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
899 | *
900 | * When an Angular application needs some data from a server, it calls the $http service, which
901 | * sends the request to a real server using $httpBackend service. With dependency injection, it is
902 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
903 | * the requests and respond with some testing data without sending a request to a real server.
904 | *
905 | * There are two ways to specify what test data should be returned as http responses by the mock
906 | * backend when the code under test makes http requests:
907 | *
908 | * - `$httpBackend.expect` - specifies a request expectation
909 | * - `$httpBackend.when` - specifies a backend definition
910 | *
911 | *
912 | * # Request Expectations vs Backend Definitions
913 | *
914 | * Request expectations provide a way to make assertions about requests made by the application and
915 | * to define responses for those requests. The test will fail if the expected requests are not made
916 | * or they are made in the wrong order.
917 | *
918 | * Backend definitions allow you to define a fake backend for your application which doesn't assert
919 | * if a particular request was made or not, it just returns a trained response if a request is made.
920 | * The test will pass whether or not the request gets made during testing.
921 | *
922 | *
923 | *
924 | * | Request expectations | Backend definitions |
925 | *
926 | * Syntax |
927 | * .expect(...).respond(...) |
928 | * .when(...).respond(...) |
929 | *
930 | *
931 | * Typical usage |
932 | * strict unit tests |
933 | * loose (black-box) unit testing |
934 | *
935 | *
936 | * Fulfills multiple requests |
937 | * NO |
938 | * YES |
939 | *
940 | *
941 | * Order of requests matters |
942 | * YES |
943 | * NO |
944 | *
945 | *
946 | * Request required |
947 | * YES |
948 | * NO |
949 | *
950 | *
951 | * Response required |
952 | * optional (see below) |
953 | * YES |
954 | *
955 | *
956 | *
957 | * In cases where both backend definitions and request expectations are specified during unit
958 | * testing, the request expectations are evaluated first.
959 | *
960 | * If a request expectation has no response specified, the algorithm will search your backend
961 | * definitions for an appropriate response.
962 | *
963 | * If a request didn't match any expectation or if the expectation doesn't have the response
964 | * defined, the backend definitions are evaluated in sequential order to see if any of them match
965 | * the request. The response from the first matched definition is returned.
966 | *
967 | *
968 | * # Flushing HTTP requests
969 | *
970 | * The $httpBackend used in production always responds to requests asynchronously. If we preserved
971 | * this behavior in unit testing, we'd have to create async unit tests, which are hard to write,
972 | * to follow and to maintain. But neither can the testing mock respond synchronously; that would
973 | * change the execution of the code under test. For this reason, the mock $httpBackend has a
974 | * `flush()` method, which allows the test to explicitly flush pending requests. This preserves
975 | * the async api of the backend, while allowing the test to execute synchronously.
976 | *
977 | *
978 | * # Unit testing with mock $httpBackend
979 | * The following code shows how to setup and use the mock backend when unit testing a controller.
980 | * First we create the controller under test:
981 | *
982 | ```js
983 | // The controller code
984 | function MyController($scope, $http) {
985 | var authToken;
986 |
987 | $http.get('/auth.py').success(function(data, status, headers) {
988 | authToken = headers('A-Token');
989 | $scope.user = data;
990 | });
991 |
992 | $scope.saveMessage = function(message) {
993 | var headers = { 'Authorization': authToken };
994 | $scope.status = 'Saving...';
995 |
996 | $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
997 | $scope.status = '';
998 | }).error(function() {
999 | $scope.status = 'ERROR!';
1000 | });
1001 | };
1002 | }
1003 | ```
1004 | *
1005 | * Now we setup the mock backend and create the test specs:
1006 | *
1007 | ```js
1008 | // testing controller
1009 | describe('MyController', function() {
1010 | var $httpBackend, $rootScope, createController;
1011 |
1012 | beforeEach(inject(function($injector) {
1013 | // Set up the mock http service responses
1014 | $httpBackend = $injector.get('$httpBackend');
1015 | // backend definition common for all tests
1016 | $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
1017 |
1018 | // Get hold of a scope (i.e. the root scope)
1019 | $rootScope = $injector.get('$rootScope');
1020 | // The $controller service is used to create instances of controllers
1021 | var $controller = $injector.get('$controller');
1022 |
1023 | createController = function() {
1024 | return $controller('MyController', {'$scope' : $rootScope });
1025 | };
1026 | }));
1027 |
1028 |
1029 | afterEach(function() {
1030 | $httpBackend.verifyNoOutstandingExpectation();
1031 | $httpBackend.verifyNoOutstandingRequest();
1032 | });
1033 |
1034 |
1035 | it('should fetch authentication token', function() {
1036 | $httpBackend.expectGET('/auth.py');
1037 | var controller = createController();
1038 | $httpBackend.flush();
1039 | });
1040 |
1041 |
1042 | it('should send msg to server', function() {
1043 | var controller = createController();
1044 | $httpBackend.flush();
1045 |
1046 | // now you don’t care about the authentication, but
1047 | // the controller will still send the request and
1048 | // $httpBackend will respond without you having to
1049 | // specify the expectation and response for this request
1050 |
1051 | $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
1052 | $rootScope.saveMessage('message content');
1053 | expect($rootScope.status).toBe('Saving...');
1054 | $httpBackend.flush();
1055 | expect($rootScope.status).toBe('');
1056 | });
1057 |
1058 |
1059 | it('should send auth header', function() {
1060 | var controller = createController();
1061 | $httpBackend.flush();
1062 |
1063 | $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
1064 | // check if the header was send, if it wasn't the expectation won't
1065 | // match the request and the test will fail
1066 | return headers['Authorization'] == 'xxx';
1067 | }).respond(201, '');
1068 |
1069 | $rootScope.saveMessage('whatever');
1070 | $httpBackend.flush();
1071 | });
1072 | });
1073 | ```
1074 | */
1075 | angular.mock.$HttpBackendProvider = function() {
1076 | this.$get = ['$rootScope', createHttpBackendMock];
1077 | };
1078 |
1079 | /**
1080 | * General factory function for $httpBackend mock.
1081 | * Returns instance for unit testing (when no arguments specified):
1082 | * - passing through is disabled
1083 | * - auto flushing is disabled
1084 | *
1085 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified):
1086 | * - passing through (delegating request to real backend) is enabled
1087 | * - auto flushing is enabled
1088 | *
1089 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified)
1090 | * @param {Object=} $browser Auto-flushing enabled if specified
1091 | * @return {Object} Instance of $httpBackend mock
1092 | */
1093 | function createHttpBackendMock($rootScope, $delegate, $browser) {
1094 | var definitions = [],
1095 | expectations = [],
1096 | responses = [],
1097 | responsesPush = angular.bind(responses, responses.push),
1098 | copy = angular.copy;
1099 |
1100 | function createResponse(status, data, headers, statusText) {
1101 | if (angular.isFunction(status)) return status;
1102 |
1103 | return function() {
1104 | return angular.isNumber(status)
1105 | ? [status, data, headers, statusText]
1106 | : [200, status, data];
1107 | };
1108 | }
1109 |
1110 | // TODO(vojta): change params to: method, url, data, headers, callback
1111 | function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) {
1112 | var xhr = new MockXhr(),
1113 | expectation = expectations[0],
1114 | wasExpected = false;
1115 |
1116 | function prettyPrint(data) {
1117 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
1118 | ? data
1119 | : angular.toJson(data);
1120 | }
1121 |
1122 | function wrapResponse(wrapped) {
1123 | if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
1124 |
1125 | return handleResponse;
1126 |
1127 | function handleResponse() {
1128 | var response = wrapped.response(method, url, data, headers);
1129 | xhr.$$respHeaders = response[2];
1130 | callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
1131 | copy(response[3] || ''));
1132 | }
1133 |
1134 | function handleTimeout() {
1135 | for (var i = 0, ii = responses.length; i < ii; i++) {
1136 | if (responses[i] === handleResponse) {
1137 | responses.splice(i, 1);
1138 | callback(-1, undefined, '');
1139 | break;
1140 | }
1141 | }
1142 | }
1143 | }
1144 |
1145 | if (expectation && expectation.match(method, url)) {
1146 | if (!expectation.matchData(data))
1147 | throw new Error('Expected ' + expectation + ' with different data\n' +
1148 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
1149 |
1150 | if (!expectation.matchHeaders(headers))
1151 | throw new Error('Expected ' + expectation + ' with different headers\n' +
1152 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
1153 | prettyPrint(headers));
1154 |
1155 | expectations.shift();
1156 |
1157 | if (expectation.response) {
1158 | responses.push(wrapResponse(expectation));
1159 | return;
1160 | }
1161 | wasExpected = true;
1162 | }
1163 |
1164 | var i = -1, definition;
1165 | while ((definition = definitions[++i])) {
1166 | if (definition.match(method, url, data, headers || {})) {
1167 | if (definition.response) {
1168 | // if $browser specified, we do auto flush all requests
1169 | ($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
1170 | } else if (definition.passThrough) {
1171 | $delegate(method, url, data, callback, headers, timeout, withCredentials);
1172 | } else throw new Error('No response defined !');
1173 | return;
1174 | }
1175 | }
1176 | throw wasExpected ?
1177 | new Error('No response defined !') :
1178 | new Error('Unexpected request: ' + method + ' ' + url + '\n' +
1179 | (expectation ? 'Expected ' + expectation : 'No more request expected'));
1180 | }
1181 |
1182 | /**
1183 | * @ngdoc method
1184 | * @name $httpBackend#when
1185 | * @description
1186 | * Creates a new backend definition.
1187 | *
1188 | * @param {string} method HTTP method.
1189 | * @param {string|RegExp} url HTTP url.
1190 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
1191 | * data string and returns true if the data is as expected.
1192 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
1193 | * object and returns true if the headers match the current definition.
1194 | * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1195 | * request is handled.
1196 | *
1197 | * - respond –
1198 | * `{function([status,] data[, headers, statusText])
1199 | * | function(function(method, url, data, headers)}`
1200 | * – The respond method takes a set of static data to be returned or a function that can
1201 | * return an array containing response status (number), response data (string), response
1202 | * headers (Object), and the text for the status (string).
1203 | */
1204 | $httpBackend.when = function(method, url, data, headers) {
1205 | var definition = new MockHttpExpectation(method, url, data, headers),
1206 | chain = {
1207 | respond: function(status, data, headers, statusText) {
1208 | definition.response = createResponse(status, data, headers, statusText);
1209 | }
1210 | };
1211 |
1212 | if ($browser) {
1213 | chain.passThrough = function() {
1214 | definition.passThrough = true;
1215 | };
1216 | }
1217 |
1218 | definitions.push(definition);
1219 | return chain;
1220 | };
1221 |
1222 | /**
1223 | * @ngdoc method
1224 | * @name $httpBackend#whenGET
1225 | * @description
1226 | * Creates a new backend definition for GET requests. For more info see `when()`.
1227 | *
1228 | * @param {string|RegExp} url HTTP url.
1229 | * @param {(Object|function(Object))=} headers HTTP headers.
1230 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1231 | * request is handled.
1232 | */
1233 |
1234 | /**
1235 | * @ngdoc method
1236 | * @name $httpBackend#whenHEAD
1237 | * @description
1238 | * Creates a new backend definition for HEAD requests. For more info see `when()`.
1239 | *
1240 | * @param {string|RegExp} url HTTP url.
1241 | * @param {(Object|function(Object))=} headers HTTP headers.
1242 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1243 | * request is handled.
1244 | */
1245 |
1246 | /**
1247 | * @ngdoc method
1248 | * @name $httpBackend#whenDELETE
1249 | * @description
1250 | * Creates a new backend definition for DELETE requests. For more info see `when()`.
1251 | *
1252 | * @param {string|RegExp} url HTTP url.
1253 | * @param {(Object|function(Object))=} headers HTTP headers.
1254 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1255 | * request is handled.
1256 | */
1257 |
1258 | /**
1259 | * @ngdoc method
1260 | * @name $httpBackend#whenPOST
1261 | * @description
1262 | * Creates a new backend definition for POST requests. For more info see `when()`.
1263 | *
1264 | * @param {string|RegExp} url HTTP url.
1265 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
1266 | * data string and returns true if the data is as expected.
1267 | * @param {(Object|function(Object))=} headers HTTP headers.
1268 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1269 | * request is handled.
1270 | */
1271 |
1272 | /**
1273 | * @ngdoc method
1274 | * @name $httpBackend#whenPUT
1275 | * @description
1276 | * Creates a new backend definition for PUT requests. For more info see `when()`.
1277 | *
1278 | * @param {string|RegExp} url HTTP url.
1279 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
1280 | * data string and returns true if the data is as expected.
1281 | * @param {(Object|function(Object))=} headers HTTP headers.
1282 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1283 | * request is handled.
1284 | */
1285 |
1286 | /**
1287 | * @ngdoc method
1288 | * @name $httpBackend#whenJSONP
1289 | * @description
1290 | * Creates a new backend definition for JSONP requests. For more info see `when()`.
1291 | *
1292 | * @param {string|RegExp} url HTTP url.
1293 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1294 | * request is handled.
1295 | */
1296 | createShortMethods('when');
1297 |
1298 |
1299 | /**
1300 | * @ngdoc method
1301 | * @name $httpBackend#expect
1302 | * @description
1303 | * Creates a new request expectation.
1304 | *
1305 | * @param {string} method HTTP method.
1306 | * @param {string|RegExp} url HTTP url.
1307 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
1308 | * receives data string and returns true if the data is as expected, or Object if request body
1309 | * is in JSON format.
1310 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
1311 | * object and returns true if the headers match the current expectation.
1312 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1313 | * request is handled.
1314 | *
1315 | * - respond –
1316 | * `{function([status,] data[, headers, statusText])
1317 | * | function(function(method, url, data, headers)}`
1318 | * – The respond method takes a set of static data to be returned or a function that can
1319 | * return an array containing response status (number), response data (string), response
1320 | * headers (Object), and the text for the status (string).
1321 | */
1322 | $httpBackend.expect = function(method, url, data, headers) {
1323 | var expectation = new MockHttpExpectation(method, url, data, headers);
1324 | expectations.push(expectation);
1325 | return {
1326 | respond: function (status, data, headers, statusText) {
1327 | expectation.response = createResponse(status, data, headers, statusText);
1328 | }
1329 | };
1330 | };
1331 |
1332 |
1333 | /**
1334 | * @ngdoc method
1335 | * @name $httpBackend#expectGET
1336 | * @description
1337 | * Creates a new request expectation for GET requests. For more info see `expect()`.
1338 | *
1339 | * @param {string|RegExp} url HTTP url.
1340 | * @param {Object=} headers HTTP headers.
1341 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1342 | * request is handled. See #expect for more info.
1343 | */
1344 |
1345 | /**
1346 | * @ngdoc method
1347 | * @name $httpBackend#expectHEAD
1348 | * @description
1349 | * Creates a new request expectation for HEAD requests. For more info see `expect()`.
1350 | *
1351 | * @param {string|RegExp} url HTTP url.
1352 | * @param {Object=} headers HTTP headers.
1353 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1354 | * request is handled.
1355 | */
1356 |
1357 | /**
1358 | * @ngdoc method
1359 | * @name $httpBackend#expectDELETE
1360 | * @description
1361 | * Creates a new request expectation for DELETE requests. For more info see `expect()`.
1362 | *
1363 | * @param {string|RegExp} url HTTP url.
1364 | * @param {Object=} headers HTTP headers.
1365 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1366 | * request is handled.
1367 | */
1368 |
1369 | /**
1370 | * @ngdoc method
1371 | * @name $httpBackend#expectPOST
1372 | * @description
1373 | * Creates a new request expectation for POST requests. For more info see `expect()`.
1374 | *
1375 | * @param {string|RegExp} url HTTP url.
1376 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
1377 | * receives data string and returns true if the data is as expected, or Object if request body
1378 | * is in JSON format.
1379 | * @param {Object=} headers HTTP headers.
1380 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1381 | * request is handled.
1382 | */
1383 |
1384 | /**
1385 | * @ngdoc method
1386 | * @name $httpBackend#expectPUT
1387 | * @description
1388 | * Creates a new request expectation for PUT requests. For more info see `expect()`.
1389 | *
1390 | * @param {string|RegExp} url HTTP url.
1391 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
1392 | * receives data string and returns true if the data is as expected, or Object if request body
1393 | * is in JSON format.
1394 | * @param {Object=} headers HTTP headers.
1395 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1396 | * request is handled.
1397 | */
1398 |
1399 | /**
1400 | * @ngdoc method
1401 | * @name $httpBackend#expectPATCH
1402 | * @description
1403 | * Creates a new request expectation for PATCH requests. For more info see `expect()`.
1404 | *
1405 | * @param {string|RegExp} url HTTP url.
1406 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
1407 | * receives data string and returns true if the data is as expected, or Object if request body
1408 | * is in JSON format.
1409 | * @param {Object=} headers HTTP headers.
1410 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1411 | * request is handled.
1412 | */
1413 |
1414 | /**
1415 | * @ngdoc method
1416 | * @name $httpBackend#expectJSONP
1417 | * @description
1418 | * Creates a new request expectation for JSONP requests. For more info see `expect()`.
1419 | *
1420 | * @param {string|RegExp} url HTTP url.
1421 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1422 | * request is handled.
1423 | */
1424 | createShortMethods('expect');
1425 |
1426 |
1427 | /**
1428 | * @ngdoc method
1429 | * @name $httpBackend#flush
1430 | * @description
1431 | * Flushes all pending requests using the trained responses.
1432 | *
1433 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined,
1434 | * all pending requests will be flushed. If there are no pending requests when the flush method
1435 | * is called an exception is thrown (as this typically a sign of programming error).
1436 | */
1437 | $httpBackend.flush = function(count) {
1438 | $rootScope.$digest();
1439 | if (!responses.length) throw new Error('No pending request to flush !');
1440 |
1441 | if (angular.isDefined(count)) {
1442 | while (count--) {
1443 | if (!responses.length) throw new Error('No more pending request to flush !');
1444 | responses.shift()();
1445 | }
1446 | } else {
1447 | while (responses.length) {
1448 | responses.shift()();
1449 | }
1450 | }
1451 | $httpBackend.verifyNoOutstandingExpectation();
1452 | };
1453 |
1454 |
1455 | /**
1456 | * @ngdoc method
1457 | * @name $httpBackend#verifyNoOutstandingExpectation
1458 | * @description
1459 | * Verifies that all of the requests defined via the `expect` api were made. If any of the
1460 | * requests were not made, verifyNoOutstandingExpectation throws an exception.
1461 | *
1462 | * Typically, you would call this method following each test case that asserts requests using an
1463 | * "afterEach" clause.
1464 | *
1465 | * ```js
1466 | * afterEach($httpBackend.verifyNoOutstandingExpectation);
1467 | * ```
1468 | */
1469 | $httpBackend.verifyNoOutstandingExpectation = function() {
1470 | $rootScope.$digest();
1471 | if (expectations.length) {
1472 | throw new Error('Unsatisfied requests: ' + expectations.join(', '));
1473 | }
1474 | };
1475 |
1476 |
1477 | /**
1478 | * @ngdoc method
1479 | * @name $httpBackend#verifyNoOutstandingRequest
1480 | * @description
1481 | * Verifies that there are no outstanding requests that need to be flushed.
1482 | *
1483 | * Typically, you would call this method following each test case that asserts requests using an
1484 | * "afterEach" clause.
1485 | *
1486 | * ```js
1487 | * afterEach($httpBackend.verifyNoOutstandingRequest);
1488 | * ```
1489 | */
1490 | $httpBackend.verifyNoOutstandingRequest = function() {
1491 | if (responses.length) {
1492 | throw new Error('Unflushed requests: ' + responses.length);
1493 | }
1494 | };
1495 |
1496 |
1497 | /**
1498 | * @ngdoc method
1499 | * @name $httpBackend#resetExpectations
1500 | * @description
1501 | * Resets all request expectations, but preserves all backend definitions. Typically, you would
1502 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of
1503 | * $httpBackend mock.
1504 | */
1505 | $httpBackend.resetExpectations = function() {
1506 | expectations.length = 0;
1507 | responses.length = 0;
1508 | };
1509 |
1510 | return $httpBackend;
1511 |
1512 |
1513 | function createShortMethods(prefix) {
1514 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
1515 | $httpBackend[prefix + method] = function(url, headers) {
1516 | return $httpBackend[prefix](method, url, undefined, headers);
1517 | };
1518 | });
1519 |
1520 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
1521 | $httpBackend[prefix + method] = function(url, data, headers) {
1522 | return $httpBackend[prefix](method, url, data, headers);
1523 | };
1524 | });
1525 | }
1526 | }
1527 |
1528 | function MockHttpExpectation(method, url, data, headers) {
1529 |
1530 | this.data = data;
1531 | this.headers = headers;
1532 |
1533 | this.match = function(m, u, d, h) {
1534 | if (method != m) return false;
1535 | if (!this.matchUrl(u)) return false;
1536 | if (angular.isDefined(d) && !this.matchData(d)) return false;
1537 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
1538 | return true;
1539 | };
1540 |
1541 | this.matchUrl = function(u) {
1542 | if (!url) return true;
1543 | if (angular.isFunction(url.test)) return url.test(u);
1544 | return url == u;
1545 | };
1546 |
1547 | this.matchHeaders = function(h) {
1548 | if (angular.isUndefined(headers)) return true;
1549 | if (angular.isFunction(headers)) return headers(h);
1550 | return angular.equals(headers, h);
1551 | };
1552 |
1553 | this.matchData = function(d) {
1554 | if (angular.isUndefined(data)) return true;
1555 | if (data && angular.isFunction(data.test)) return data.test(d);
1556 | if (data && angular.isFunction(data)) return data(d);
1557 | if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
1558 | return data == d;
1559 | };
1560 |
1561 | this.toString = function() {
1562 | return method + ' ' + url;
1563 | };
1564 | }
1565 |
1566 | function createMockXhr() {
1567 | return new MockXhr();
1568 | }
1569 |
1570 | function MockXhr() {
1571 |
1572 | // hack for testing $http, $httpBackend
1573 | MockXhr.$$lastInstance = this;
1574 |
1575 | this.open = function(method, url, async) {
1576 | this.$$method = method;
1577 | this.$$url = url;
1578 | this.$$async = async;
1579 | this.$$reqHeaders = {};
1580 | this.$$respHeaders = {};
1581 | };
1582 |
1583 | this.send = function(data) {
1584 | this.$$data = data;
1585 | };
1586 |
1587 | this.setRequestHeader = function(key, value) {
1588 | this.$$reqHeaders[key] = value;
1589 | };
1590 |
1591 | this.getResponseHeader = function(name) {
1592 | // the lookup must be case insensitive,
1593 | // that's why we try two quick lookups first and full scan last
1594 | var header = this.$$respHeaders[name];
1595 | if (header) return header;
1596 |
1597 | name = angular.lowercase(name);
1598 | header = this.$$respHeaders[name];
1599 | if (header) return header;
1600 |
1601 | header = undefined;
1602 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
1603 | if (!header && angular.lowercase(headerName) == name) header = headerVal;
1604 | });
1605 | return header;
1606 | };
1607 |
1608 | this.getAllResponseHeaders = function() {
1609 | var lines = [];
1610 |
1611 | angular.forEach(this.$$respHeaders, function(value, key) {
1612 | lines.push(key + ': ' + value);
1613 | });
1614 | return lines.join('\n');
1615 | };
1616 |
1617 | this.abort = angular.noop;
1618 | }
1619 |
1620 |
1621 | /**
1622 | * @ngdoc service
1623 | * @name $timeout
1624 | * @description
1625 | *
1626 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service
1627 | * that adds a "flush" and "verifyNoPendingTasks" methods.
1628 | */
1629 |
1630 | angular.mock.$TimeoutDecorator = function($delegate, $browser) {
1631 |
1632 | /**
1633 | * @ngdoc method
1634 | * @name $timeout#flush
1635 | * @description
1636 | *
1637 | * Flushes the queue of pending tasks.
1638 | *
1639 | * @param {number=} delay maximum timeout amount to flush up until
1640 | */
1641 | $delegate.flush = function(delay) {
1642 | $browser.defer.flush(delay);
1643 | };
1644 |
1645 | /**
1646 | * @ngdoc method
1647 | * @name $timeout#verifyNoPendingTasks
1648 | * @description
1649 | *
1650 | * Verifies that there are no pending tasks that need to be flushed.
1651 | */
1652 | $delegate.verifyNoPendingTasks = function() {
1653 | if ($browser.deferredFns.length) {
1654 | throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
1655 | formatPendingTasksAsString($browser.deferredFns));
1656 | }
1657 | };
1658 |
1659 | function formatPendingTasksAsString(tasks) {
1660 | var result = [];
1661 | angular.forEach(tasks, function(task) {
1662 | result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}');
1663 | });
1664 |
1665 | return result.join(', ');
1666 | }
1667 |
1668 | return $delegate;
1669 | };
1670 |
1671 | angular.mock.$RAFDecorator = function($delegate) {
1672 | var queue = [];
1673 | var rafFn = function(fn) {
1674 | var index = queue.length;
1675 | queue.push(fn);
1676 | return function() {
1677 | queue.splice(index, 1);
1678 | };
1679 | };
1680 |
1681 | rafFn.supported = $delegate.supported;
1682 |
1683 | rafFn.flush = function() {
1684 | if(queue.length === 0) {
1685 | throw new Error('No rAF callbacks present');
1686 | }
1687 |
1688 | var length = queue.length;
1689 | for(var i=0;i');
1719 | };
1720 | };
1721 |
1722 | /**
1723 | * @ngdoc module
1724 | * @name ngMock
1725 | * @packageName angular-mocks
1726 | * @description
1727 | *
1728 | * # ngMock
1729 | *
1730 | * The `ngMock` module provides support to inject and mock Angular services into unit tests.
1731 | * In addition, ngMock also extends various core ng services such that they can be
1732 | * inspected and controlled in a synchronous manner within test code.
1733 | *
1734 | *
1735 | *
1736 | *
1737 | */
1738 | angular.module('ngMock', ['ng']).provider({
1739 | $browser: angular.mock.$BrowserProvider,
1740 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
1741 | $log: angular.mock.$LogProvider,
1742 | $interval: angular.mock.$IntervalProvider,
1743 | $httpBackend: angular.mock.$HttpBackendProvider,
1744 | $rootElement: angular.mock.$RootElementProvider
1745 | }).config(['$provide', function($provide) {
1746 | $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
1747 | $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
1748 | $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
1749 | }]);
1750 |
1751 | /**
1752 | * @ngdoc module
1753 | * @name ngMockE2E
1754 | * @module ngMockE2E
1755 | * @packageName angular-mocks
1756 | * @description
1757 | *
1758 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing.
1759 | * Currently there is only one mock present in this module -
1760 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock.
1761 | */
1762 | angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
1763 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
1764 | }]);
1765 |
1766 | /**
1767 | * @ngdoc service
1768 | * @name $httpBackend
1769 | * @module ngMockE2E
1770 | * @description
1771 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of
1772 | * applications that use the {@link ng.$http $http service}.
1773 | *
1774 | * *Note*: For fake http backend implementation suitable for unit testing please see
1775 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}.
1776 | *
1777 | * This implementation can be used to respond with static or dynamic responses via the `when` api
1778 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the
1779 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch
1780 | * templates from a webserver).
1781 | *
1782 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application
1783 | * is being developed with the real backend api replaced with a mock, it is often desirable for
1784 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch
1785 | * templates or static files from the webserver). To configure the backend with this behavior
1786 | * use the `passThrough` request handler of `when` instead of `respond`.
1787 | *
1788 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit
1789 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests
1790 | * automatically, closely simulating the behavior of the XMLHttpRequest object.
1791 | *
1792 | * To setup the application to run with this http backend, you have to create a module that depends
1793 | * on the `ngMockE2E` and your application modules and defines the fake backend:
1794 | *
1795 | * ```js
1796 | * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1797 | * myAppDev.run(function($httpBackend) {
1798 | * phones = [{name: 'phone1'}, {name: 'phone2'}];
1799 | *
1800 | * // returns the current list of phones
1801 | * $httpBackend.whenGET('/phones').respond(phones);
1802 | *
1803 | * // adds a new phone to the phones array
1804 | * $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1805 | * var phone = angular.fromJson(data);
1806 | * phones.push(phone);
1807 | * return [200, phone, {}];
1808 | * });
1809 | * $httpBackend.whenGET(/^\/templates\//).passThrough();
1810 | * //...
1811 | * });
1812 | * ```
1813 | *
1814 | * Afterwards, bootstrap your app with this new module.
1815 | */
1816 |
1817 | /**
1818 | * @ngdoc method
1819 | * @name $httpBackend#when
1820 | * @module ngMockE2E
1821 | * @description
1822 | * Creates a new backend definition.
1823 | *
1824 | * @param {string} method HTTP method.
1825 | * @param {string|RegExp} url HTTP url.
1826 | * @param {(string|RegExp)=} data HTTP request body.
1827 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
1828 | * object and returns true if the headers match the current definition.
1829 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1830 | * control how a matched request is handled.
1831 | *
1832 | * - respond –
1833 | * `{function([status,] data[, headers, statusText])
1834 | * | function(function(method, url, data, headers)}`
1835 | * – The respond method takes a set of static data to be returned or a function that can return
1836 | * an array containing response status (number), response data (string), response headers
1837 | * (Object), and the text for the status (string).
1838 | * - passThrough – `{function()}` – Any request matching a backend definition with
1839 | * `passThrough` handler will be passed through to the real backend (an XHR request will be made
1840 | * to the server.)
1841 | */
1842 |
1843 | /**
1844 | * @ngdoc method
1845 | * @name $httpBackend#whenGET
1846 | * @module ngMockE2E
1847 | * @description
1848 | * Creates a new backend definition for GET requests. For more info see `when()`.
1849 | *
1850 | * @param {string|RegExp} url HTTP url.
1851 | * @param {(Object|function(Object))=} headers HTTP headers.
1852 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1853 | * control how a matched request is handled.
1854 | */
1855 |
1856 | /**
1857 | * @ngdoc method
1858 | * @name $httpBackend#whenHEAD
1859 | * @module ngMockE2E
1860 | * @description
1861 | * Creates a new backend definition for HEAD requests. For more info see `when()`.
1862 | *
1863 | * @param {string|RegExp} url HTTP url.
1864 | * @param {(Object|function(Object))=} headers HTTP headers.
1865 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1866 | * control how a matched request is handled.
1867 | */
1868 |
1869 | /**
1870 | * @ngdoc method
1871 | * @name $httpBackend#whenDELETE
1872 | * @module ngMockE2E
1873 | * @description
1874 | * Creates a new backend definition for DELETE requests. For more info see `when()`.
1875 | *
1876 | * @param {string|RegExp} url HTTP url.
1877 | * @param {(Object|function(Object))=} headers HTTP headers.
1878 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1879 | * control how a matched request is handled.
1880 | */
1881 |
1882 | /**
1883 | * @ngdoc method
1884 | * @name $httpBackend#whenPOST
1885 | * @module ngMockE2E
1886 | * @description
1887 | * Creates a new backend definition for POST requests. For more info see `when()`.
1888 | *
1889 | * @param {string|RegExp} url HTTP url.
1890 | * @param {(string|RegExp)=} data HTTP request body.
1891 | * @param {(Object|function(Object))=} headers HTTP headers.
1892 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1893 | * control how a matched request is handled.
1894 | */
1895 |
1896 | /**
1897 | * @ngdoc method
1898 | * @name $httpBackend#whenPUT
1899 | * @module ngMockE2E
1900 | * @description
1901 | * Creates a new backend definition for PUT requests. For more info see `when()`.
1902 | *
1903 | * @param {string|RegExp} url HTTP url.
1904 | * @param {(string|RegExp)=} data HTTP request body.
1905 | * @param {(Object|function(Object))=} headers HTTP headers.
1906 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1907 | * control how a matched request is handled.
1908 | */
1909 |
1910 | /**
1911 | * @ngdoc method
1912 | * @name $httpBackend#whenPATCH
1913 | * @module ngMockE2E
1914 | * @description
1915 | * Creates a new backend definition for PATCH requests. For more info see `when()`.
1916 | *
1917 | * @param {string|RegExp} url HTTP url.
1918 | * @param {(string|RegExp)=} data HTTP request body.
1919 | * @param {(Object|function(Object))=} headers HTTP headers.
1920 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1921 | * control how a matched request is handled.
1922 | */
1923 |
1924 | /**
1925 | * @ngdoc method
1926 | * @name $httpBackend#whenJSONP
1927 | * @module ngMockE2E
1928 | * @description
1929 | * Creates a new backend definition for JSONP requests. For more info see `when()`.
1930 | *
1931 | * @param {string|RegExp} url HTTP url.
1932 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1933 | * control how a matched request is handled.
1934 | */
1935 | angular.mock.e2e = {};
1936 | angular.mock.e2e.$httpBackendDecorator =
1937 | ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
1938 |
1939 |
1940 | angular.mock.clearDataCache = function() {
1941 | var key,
1942 | cache = angular.element.cache;
1943 |
1944 | for(key in cache) {
1945 | if (Object.prototype.hasOwnProperty.call(cache,key)) {
1946 | var handle = cache[key].handle;
1947 |
1948 | handle && angular.element(handle.elem).off();
1949 | delete cache[key];
1950 | }
1951 | }
1952 | };
1953 |
1954 |
1955 | if(window.jasmine || window.mocha) {
1956 |
1957 | var currentSpec = null,
1958 | isSpecRunning = function() {
1959 | return !!currentSpec;
1960 | };
1961 |
1962 |
1963 | (window.beforeEach || window.setup)(function() {
1964 | currentSpec = this;
1965 | });
1966 |
1967 | (window.afterEach || window.teardown)(function() {
1968 | var injector = currentSpec.$injector;
1969 |
1970 | angular.forEach(currentSpec.$modules, function(module) {
1971 | if (module && module.$$hashKey) {
1972 | module.$$hashKey = undefined;
1973 | }
1974 | });
1975 |
1976 | currentSpec.$injector = null;
1977 | currentSpec.$modules = null;
1978 | currentSpec = null;
1979 |
1980 | if (injector) {
1981 | injector.get('$rootElement').off();
1982 | injector.get('$browser').pollFns.length = 0;
1983 | }
1984 |
1985 | angular.mock.clearDataCache();
1986 |
1987 | // clean up jquery's fragment cache
1988 | angular.forEach(angular.element.fragments, function(val, key) {
1989 | delete angular.element.fragments[key];
1990 | });
1991 |
1992 | MockXhr.$$lastInstance = null;
1993 |
1994 | angular.forEach(angular.callbacks, function(val, key) {
1995 | delete angular.callbacks[key];
1996 | });
1997 | angular.callbacks.counter = 0;
1998 | });
1999 |
2000 | /**
2001 | * @ngdoc function
2002 | * @name angular.mock.module
2003 | * @description
2004 | *
2005 | * *NOTE*: This function is also published on window for easy access.
2006 | *
2007 | * This function registers a module configuration code. It collects the configuration information
2008 | * which will be used when the injector is created by {@link angular.mock.inject inject}.
2009 | *
2010 | * See {@link angular.mock.inject inject} for usage example
2011 | *
2012 | * @param {...(string|Function|Object)} fns any number of modules which are represented as string
2013 | * aliases or as anonymous module initialization functions. The modules are used to
2014 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
2015 | * object literal is passed they will be registered as values in the module, the key being
2016 | * the module name and the value being what is returned.
2017 | */
2018 | window.module = angular.mock.module = function() {
2019 | var moduleFns = Array.prototype.slice.call(arguments, 0);
2020 | return isSpecRunning() ? workFn() : workFn;
2021 | /////////////////////
2022 | function workFn() {
2023 | if (currentSpec.$injector) {
2024 | throw new Error('Injector already created, can not register a module!');
2025 | } else {
2026 | var modules = currentSpec.$modules || (currentSpec.$modules = []);
2027 | angular.forEach(moduleFns, function(module) {
2028 | if (angular.isObject(module) && !angular.isArray(module)) {
2029 | modules.push(function($provide) {
2030 | angular.forEach(module, function(value, key) {
2031 | $provide.value(key, value);
2032 | });
2033 | });
2034 | } else {
2035 | modules.push(module);
2036 | }
2037 | });
2038 | }
2039 | }
2040 | };
2041 |
2042 | /**
2043 | * @ngdoc function
2044 | * @name angular.mock.inject
2045 | * @description
2046 | *
2047 | * *NOTE*: This function is also published on window for easy access.
2048 | *
2049 | * The inject function wraps a function into an injectable function. The inject() creates new
2050 | * instance of {@link auto.$injector $injector} per test, which is then used for
2051 | * resolving references.
2052 | *
2053 | *
2054 | * ## Resolving References (Underscore Wrapping)
2055 | * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this
2056 | * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable
2057 | * that is declared in the scope of the `describe()` block. Since we would, most likely, want
2058 | * the variable to have the same name of the reference we have a problem, since the parameter
2059 | * to the `inject()` function would hide the outer variable.
2060 | *
2061 | * To help with this, the injected parameters can, optionally, be enclosed with underscores.
2062 | * These are ignored by the injector when the reference name is resolved.
2063 | *
2064 | * For example, the parameter `_myService_` would be resolved as the reference `myService`.
2065 | * Since it is available in the function body as _myService_, we can then assign it to a variable
2066 | * defined in an outer scope.
2067 | *
2068 | * ```
2069 | * // Defined out reference variable outside
2070 | * var myService;
2071 | *
2072 | * // Wrap the parameter in underscores
2073 | * beforeEach( inject( function(_myService_){
2074 | * myService = _myService_;
2075 | * }));
2076 | *
2077 | * // Use myService in a series of tests.
2078 | * it('makes use of myService', function() {
2079 | * myService.doStuff();
2080 | * });
2081 | *
2082 | * ```
2083 | *
2084 | * See also {@link angular.mock.module angular.mock.module}
2085 | *
2086 | * ## Example
2087 | * Example of what a typical jasmine tests looks like with the inject method.
2088 | * ```js
2089 | *
2090 | * angular.module('myApplicationModule', [])
2091 | * .value('mode', 'app')
2092 | * .value('version', 'v1.0.1');
2093 | *
2094 | *
2095 | * describe('MyApp', function() {
2096 | *
2097 | * // You need to load modules that you want to test,
2098 | * // it loads only the "ng" module by default.
2099 | * beforeEach(module('myApplicationModule'));
2100 | *
2101 | *
2102 | * // inject() is used to inject arguments of all given functions
2103 | * it('should provide a version', inject(function(mode, version) {
2104 | * expect(version).toEqual('v1.0.1');
2105 | * expect(mode).toEqual('app');
2106 | * }));
2107 | *
2108 | *
2109 | * // The inject and module method can also be used inside of the it or beforeEach
2110 | * it('should override a version and test the new version is injected', function() {
2111 | * // module() takes functions or strings (module aliases)
2112 | * module(function($provide) {
2113 | * $provide.value('version', 'overridden'); // override version here
2114 | * });
2115 | *
2116 | * inject(function(version) {
2117 | * expect(version).toEqual('overridden');
2118 | * });
2119 | * });
2120 | * });
2121 | *
2122 | * ```
2123 | *
2124 | * @param {...Function} fns any number of functions which will be injected using the injector.
2125 | */
2126 |
2127 |
2128 |
2129 | var ErrorAddingDeclarationLocationStack = function(e, errorForStack) {
2130 | this.message = e.message;
2131 | this.name = e.name;
2132 | if (e.line) this.line = e.line;
2133 | if (e.sourceId) this.sourceId = e.sourceId;
2134 | if (e.stack && errorForStack)
2135 | this.stack = e.stack + '\n' + errorForStack.stack;
2136 | if (e.stackArray) this.stackArray = e.stackArray;
2137 | };
2138 | ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString;
2139 |
2140 | window.inject = angular.mock.inject = function() {
2141 | var blockFns = Array.prototype.slice.call(arguments, 0);
2142 | var errorForStack = new Error('Declaration Location');
2143 | return isSpecRunning() ? workFn.call(currentSpec) : workFn;
2144 | /////////////////////
2145 | function workFn() {
2146 | var modules = currentSpec.$modules || [];
2147 |
2148 | modules.unshift('ngMock');
2149 | modules.unshift('ng');
2150 | var injector = currentSpec.$injector;
2151 | if (!injector) {
2152 | injector = currentSpec.$injector = angular.injector(modules);
2153 | }
2154 | for(var i = 0, ii = blockFns.length; i < ii; i++) {
2155 | try {
2156 | /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
2157 | injector.invoke(blockFns[i] || angular.noop, this);
2158 | /* jshint +W040 */
2159 | } catch (e) {
2160 | if (e.stack && errorForStack) {
2161 | throw new ErrorAddingDeclarationLocationStack(e, errorForStack);
2162 | }
2163 | throw e;
2164 | } finally {
2165 | errorForStack = null;
2166 | }
2167 | }
2168 | }
2169 | };
2170 | }
2171 |
2172 |
2173 | })(window, window.angular);
2174 |
--------------------------------------------------------------------------------
/test/lib/browser_trigger.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
3 |
4 | function indexOf(array, obj) {
5 | if (array.indexOf) return array.indexOf(obj);
6 |
7 | for ( var i = 0; i < array.length; i++) {
8 | if (obj === array[i]) return i;
9 | }
10 | return -1;
11 | }
12 |
13 |
14 |
15 | /**
16 | * Triggers a browser event. Attempts to choose the right event if one is
17 | * not specified.
18 | *
19 | * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement
20 | * @param {string} eventType Optional event type
21 | * @param {Object=} eventData An optional object which contains additional event data (such as x,y
22 | * coordinates, keys, etc...) that are passed into the event when triggered
23 | */
24 | window.browserTrigger = function browserTrigger(element, eventType, eventData) {
25 | if (element && !element.nodeName) element = element[0];
26 | if (!element) return;
27 |
28 | eventData = eventData || {};
29 | var keys = eventData.keys;
30 | var x = eventData.x;
31 | var y = eventData.y;
32 |
33 | var inputType = (element.type) ? element.type.toLowerCase() : null,
34 | nodeName = element.nodeName.toLowerCase();
35 |
36 | if (!eventType) {
37 | eventType = {
38 | 'text': 'change',
39 | 'textarea': 'change',
40 | 'hidden': 'change',
41 | 'password': 'change',
42 | 'button': 'click',
43 | 'submit': 'click',
44 | 'reset': 'click',
45 | 'image': 'click',
46 | 'checkbox': 'click',
47 | 'radio': 'click',
48 | 'select-one': 'change',
49 | 'select-multiple': 'change',
50 | '_default_': 'click'
51 | }[inputType || '_default_'];
52 | }
53 |
54 | if (nodeName == 'option') {
55 | element.parentNode.value = element.value;
56 | element = element.parentNode;
57 | eventType = 'change';
58 | }
59 |
60 | keys = keys || [];
61 | function pressed(key) {
62 | return indexOf(keys, key) !== -1;
63 | }
64 |
65 | if (msie < 9) {
66 | if (inputType == 'radio' || inputType == 'checkbox') {
67 | element.checked = !element.checked;
68 | }
69 |
70 | // WTF!!! Error: Unspecified error.
71 | // Don't know why, but some elements when detached seem to be in inconsistent state and
72 | // calling .fireEvent() on them will result in very unhelpful error (Error: Unspecified error)
73 | // forcing the browser to compute the element position (by reading its CSS)
74 | // puts the element in consistent state.
75 | element.style.posLeft;
76 |
77 | // TODO(vojta): create event objects with pressed keys to get it working on IE<9
78 | var ret = element.fireEvent('on' + eventType);
79 | if (inputType == 'submit') {
80 | while(element) {
81 | if (element.nodeName.toLowerCase() == 'form') {
82 | element.fireEvent('onsubmit');
83 | break;
84 | }
85 | element = element.parentNode;
86 | }
87 | }
88 | return ret;
89 | } else {
90 | var evnt;
91 | if(/transitionend/.test(eventType)) {
92 | if(window.WebKitTransitionEvent) {
93 | evnt = new WebKitTransitionEvent(eventType, eventData);
94 | evnt.initEvent(eventType, false, true);
95 | }
96 | else {
97 | try {
98 | evnt = new TransitionEvent(eventType, eventData);
99 | }
100 | catch(e) {
101 | evnt = document.createEvent('TransitionEvent');
102 | evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0);
103 | }
104 | }
105 | }
106 | else if(/animationend/.test(eventType)) {
107 | if(window.WebKitAnimationEvent) {
108 | evnt = new WebKitAnimationEvent(eventType, eventData);
109 | evnt.initEvent(eventType, false, true);
110 | }
111 | else {
112 | try {
113 | evnt = new AnimationEvent(eventType, eventData);
114 | }
115 | catch(e) {
116 | evnt = document.createEvent('AnimationEvent');
117 | evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0);
118 | }
119 | }
120 | }
121 | else {
122 | evnt = document.createEvent('MouseEvents');
123 | x = x || 0;
124 | y = y || 0;
125 | evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
126 | pressed('alt'), pressed('shift'), pressed('meta'), 0, element);
127 | }
128 |
129 | /* we're unable to change the timeStamp value directly so this
130 | * is only here to allow for testing where the timeStamp value is
131 | * read */
132 | evnt.$manualTimeStamp = eventData.timeStamp;
133 |
134 | if(!evnt) return;
135 |
136 | var originalPreventDefault = evnt.preventDefault,
137 | appWindow = element.ownerDocument.defaultView,
138 | fakeProcessDefault = true,
139 | finalProcessDefault,
140 | angular = appWindow.angular || {};
141 |
142 | // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208
143 | angular['ff-684208-preventDefault'] = false;
144 | evnt.preventDefault = function() {
145 | fakeProcessDefault = false;
146 | return originalPreventDefault.apply(evnt, arguments);
147 | };
148 |
149 | element.dispatchEvent(evnt);
150 | finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault);
151 |
152 | delete angular['ff-684208-preventDefault'];
153 |
154 | return finalProcessDefault;
155 | }
156 | };
157 | }());
158 |
--------------------------------------------------------------------------------
/test/pickadate-date_helper.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint expr: true */
2 |
3 | describe('pickadate.dateHelper', function () {
4 | 'use strict';
5 | var dateHelper = null,
6 | format = 'yyyy-MM-dd';
7 |
8 | beforeEach(module("pickadate"));
9 |
10 | beforeEach(inject(function (_pickadateDateHelper_) {
11 | dateHelper = _pickadateDateHelper_;
12 | }));
13 |
14 |
15 | describe('parseDate', function() {
16 |
17 | [
18 | ['2014-02-04', null],
19 | ['2014-02-04', 'yyyy-MM-dd'],
20 | ['2014-04-02', 'yyyy-dd-MM'],
21 | ['2014/02/04', 'yyyy/MM/dd'],
22 | ['2014/04/02', 'yyyy/dd/MM'],
23 | ['04/02/2014', 'dd/MM/yyyy'],
24 | ['04-02-2014', 'dd-MM-yyyy']
25 | ].forEach(function(format) {
26 |
27 | it("parses the string in " + format[1] + " format and return a date object", function() {
28 | var dateString = format[0],
29 | date = dateHelper(format[1]).parseDate(dateString);
30 |
31 | expect(date.getDate()).to.equal(4);
32 | expect(date.getMonth()).to.equal(1);
33 | expect(date.getFullYear()).to.equal(2014);
34 | expect(date.getHours()).to.equal(3);
35 | });
36 |
37 | });
38 |
39 | it("returns undefined if a falsey object is passed", function() {
40 | expect(dateHelper().parseDate(null)).to.be.undefined;
41 | expect(dateHelper().parseDate(undefined)).to.be.undefined;
42 | });
43 |
44 | it("returns undefined if an invalid date is passed", function() {
45 | expect(dateHelper().parseDate('2014-02-')).to.be.undefined;
46 | });
47 |
48 | it("returns a new date if a date object is passed", function() {
49 | var date = new Date();
50 |
51 | expect(dateHelper().parseDate(date).getTime()).to.equal(date.getTime());
52 | });
53 |
54 | });
55 |
56 | describe('buildDayNames', function() {
57 |
58 | it('builds the days', function() {
59 | var expectedResult = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
60 | expect(dateHelper().buildDayNames()).to.deep.equal(expectedResult);
61 | });
62 |
63 | it('rotates the days', function() {
64 | var expectedResult = ['Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue'];
65 | expect(dateHelper(format, { weekStartsOn: 3 }).buildDayNames()).to.deep.equal(expectedResult);
66 |
67 | expectedResult = ['Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu'];
68 | expect(dateHelper(format, { weekStartsOn: 5 }).buildDayNames()).to.deep.equal(expectedResult);
69 | });
70 |
71 | });
72 |
73 | describe('buildDates', function() {
74 |
75 | function d(date) { return dateHelper().parseDate(date); }
76 |
77 | it('returns the correct dates', function() {
78 | var dates = dateHelper().buildDates(2015, 0, { weekStartsOn: 0 });
79 |
80 | expect(dates[0].date).to.deep.equal(d('2014-12-28'));
81 | expect(dates[3].date).to.deep.equal(d('2014-12-31'));
82 | expect(dates[4].date).to.deep.equal(d('2015-01-01'));
83 | expect(dates[34].date).to.deep.equal(d('2015-01-31'));
84 | expect(dates[35].date).to.deep.equal(d('2015-02-01'));
85 | expect(dates.slice(-1)[0].date).to.deep.equal(d('2015-02-07'));
86 | });
87 |
88 | it('has 6 rows of dates by default', function() {
89 | expect(dateHelper(format, { weekStartsOn: 0 }).buildDates(2015, 0)).to.have.length(6 * 7);
90 | });
91 |
92 | it('should not add empty rows when told not to', function() {
93 | expect(dateHelper(format, { weekStartsOn: 0, noExtraRows: true }).buildDates(2015, 0)).to.have.length(5 * 7);
94 | });
95 |
96 | it('adds 2 extra rows when required', function() {
97 | expect(dateHelper(format, { noExtraRows: false }).buildDates(2015, 1)).to.have.length(6 * 7);
98 | expect(dateHelper(format, { noExtraRows: true }).buildDates(2015, 1)).to.have.length(4 * 7);
99 | expect(dateHelper(format, { weekStartsOn: 1, noExtraRows: true }).buildDates(2015, 1)).to.have.length(5 * 7);
100 | });
101 |
102 | it('works when the week starts on monday', function() {
103 | var dates = dateHelper(format, { weekStartsOn: 1 }).buildDates(2015, 0);
104 |
105 | expect(dates[0].date).to.deep.equal(d('2014-12-29'));
106 | expect(dates[3].date).to.deep.equal(d('2015-01-01'));
107 | expect(dates[33].date).to.deep.equal(d('2015-01-31'));
108 | expect(dates.slice(-1)[0].date).to.deep.equal(d('2015-02-08'));
109 | });
110 |
111 | it('works when the week starts on saturday', function() {
112 | var dates = dateHelper(format, { weekStartsOn: 6 }).buildDates(2015, 0);
113 |
114 | expect(dates[0].date).to.deep.equal(d('2014-12-27'));
115 | expect(dates[5].date).to.deep.equal(d('2015-01-01'));
116 | expect(dates[35].date).to.deep.equal(d('2015-01-31'));
117 | expect(dates.slice(-1)[0].date).to.deep.equal(d('2015-02-06'));
118 | });
119 |
120 | });
121 |
122 | });
123 |
--------------------------------------------------------------------------------