diff --git a/config/karma-docs.conf.js b/config/karma-docs.conf.js index 7016cb4a007..fc3dc226b19 100644 --- a/config/karma-docs.conf.js +++ b/config/karma-docs.conf.js @@ -18,6 +18,7 @@ module.exports = function(config) { 'node_modules/angular-messages/angular-messages.js', 'node_modules/angular-route/angular-route.js', 'node_modules/angular-mocks/angular-mocks.js', + 'node_modules/moment/moment.js', 'dist/angular-material.js', 'config/test-utils.js', 'dist/docs/docs.js', diff --git a/config/karma.conf.js b/config/karma.conf.js index 7cd94fe59cc..838e70ff163 100644 --- a/config/karma.conf.js +++ b/config/karma.conf.js @@ -38,6 +38,7 @@ module.exports = function(config) { 'node_modules/angular-sanitize/angular-sanitize.js', 'node_modules/angular-touch/angular-touch.js', 'node_modules/angular-mocks/angular-mocks.js', + 'node_modules/moment/moment.js', 'test/angular-material-mocks.js', 'test/angular-material-spec.js' ]); diff --git a/src/components/datepicker/js/dateLocale.spec.js b/src/components/datepicker/js/dateLocale.spec.js index 9cbb9a21c06..26c029ffa4b 100644 --- a/src/components/datepicker/js/dateLocale.spec.js +++ b/src/components/datepicker/js/dateLocale.spec.js @@ -1,4 +1,3 @@ - describe('$mdDateLocale', function() { var dateLocale, dateUtil; @@ -81,7 +80,7 @@ describe('$mdDateLocale', function() { describe('with custom values', function() { var fakeMonths = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']; - var fakeshortMonths = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'j', 'l']; + var fakeShortMonths = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'j', 'l']; var fakeDays = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7']; var fakeShortDays = ['1', '2', '3', '4', '5', '6', '7']; var fakeDates = [undefined, 'X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', 'X11', @@ -90,7 +89,7 @@ describe('$mdDateLocale', function() { beforeEach(module(function($mdDateLocaleProvider) { $mdDateLocaleProvider.months = fakeMonths; - $mdDateLocaleProvider.shortMonths = fakeshortMonths; + $mdDateLocaleProvider.shortMonths = fakeShortMonths; $mdDateLocaleProvider.days = fakeDays; $mdDateLocaleProvider.shortDays = fakeShortDays; $mdDateLocaleProvider.dates = fakeDates; @@ -113,7 +112,7 @@ describe('$mdDateLocale', function() { it('should expose custom settings', function() { expect(dateLocale.months).toEqual(fakeMonths); - expect(dateLocale.shortMonths).toEqual(fakeshortMonths); + expect(dateLocale.shortMonths).toEqual(fakeShortMonths); expect(dateLocale.days).toEqual(fakeDays); expect(dateLocale.shortDays).toEqual(fakeShortDays); expect(dateLocale.dates).toEqual(fakeDates); @@ -124,4 +123,38 @@ describe('$mdDateLocale', function() { expect(dateLocale.isDateComplete('Anything Else')).toBe(false); }); }); + + describe('with MomentJS custom formatting', function() { + beforeEach(module(function($mdDateLocaleProvider) { + $mdDateLocaleProvider.formatDate = function(date) { + return date ? moment(date).format('M/D') : ''; + }; + $mdDateLocaleProvider.parseDate = function(dateString) { + var m = moment(dateString, 'M/D', true); + return m.isValid() ? m.toDate() : new Date(NaN); + }; + $mdDateLocaleProvider.isDateComplete = function(dateString) { + dateString = dateString.trim(); + // Look for two chunks of content (either numbers or text) separated by delimiters. + var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/; + return re.test(dateString); + }; + })); + + beforeEach(inject(function($mdDateLocale, $$mdDateUtil) { + dateLocale = $mdDateLocale; + dateUtil = $$mdDateUtil; + })); + + it('should respect custom formatting', function() { + var now = new Date(); + expect(dateLocale.formatDate(new Date('2020-08-31T00:00:00-04:00'))).toEqual('8/31'); + expect(dateLocale.parseDate('8/31')).toEqual(new Date(now.getFullYear(), 7, 31)); + expect(dateLocale.parseDate('1/1')).toEqual(new Date(now.getFullYear(), 0, 1)); + expect(dateLocale.isDateComplete('8/31')).toBe(true); + expect(dateLocale.isDateComplete('8-31')).toBe(true); + expect(dateLocale.isDateComplete('August_31st')).toBe(false); + expect(dateLocale.isDateComplete('2020')).toBe(false); + }); + }); }); diff --git a/src/components/datepicker/js/dateUtil.js b/src/components/datepicker/js/dateUtil.js index 9836bbafb2d..e0e345b184b 100644 --- a/src/components/datepicker/js/dateUtil.js +++ b/src/components/datepicker/js/dateUtil.js @@ -314,7 +314,12 @@ * @return {Date} date with local timezone offset removed */ function removeLocalTzAndReparseDate(value) { - return $mdDateLocale.parseDate(value.getTime() + 60000 * value.getTimezoneOffset()); + var dateValue, formattedDate; + // Remove the local timezone offset before calling formatDate. + dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset()); + formattedDate = $mdDateLocale.formatDate(dateValue); + // parseDate only works with a date formatted by formatDate when using Moment validation. + return $mdDateLocale.parseDate(formattedDate); } }); })(); diff --git a/src/components/datepicker/js/datepickerDirective.spec.js b/src/components/datepicker/js/datepickerDirective.spec.js index 5c552a74ffb..3f3466aa9d0 100644 --- a/src/components/datepicker/js/datepickerDirective.spec.js +++ b/src/components/datepicker/js/datepickerDirective.spec.js @@ -1,29 +1,53 @@ +// When constructing a Date, the month is zero-based. This can be confusing, since people are +// used to seeing them one-based. So we create these aliases to make reading the tests easier. +var JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9, + NOV = 10, DEC = 11; + +var initialDate = new Date(2015, FEB, 15); + +var ngElement, element, scope, pageScope, controller; +var $compile, $timeout, $$rAF, $animate, $window, keyCodes, dateUtil, dateLocale; + +var DATEPICKER_TEMPLATE = + '' + + ''; + +/** + * Compile and link the given template and store values for element, scope, and controller. + * @param {string} template + * @returns {JQLite} The root compiled element. + */ +function createDatepickerInstance(template) { + var outputElement = $compile(template)(pageScope); + pageScope.$apply(); + + ngElement = outputElement[0].tagName === 'MD-DATEPICKER' ? + outputElement : outputElement.find('md-datepicker'); + element = ngElement[0]; + scope = ngElement.isolateScope(); + controller = ngElement.controller('mdDatepicker'); + + return outputElement; +} + +/** Populates the inputElement with a value and triggers the input events. */ +function populateInputElement(inputString) { + controller.ngInputElement.val(inputString).triggerHandler('input'); + $timeout.flush(); + pageScope.$apply(); +} describe('md-datepicker', function() { - // When constructing a Date, the month is zero-based. This can be confusing, since people are - // used to seeing them one-based. So we create these aliases to make reading the tests easier. - var JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9, - NOV = 10, DEC = 11; - - var initialDate = new Date(2015, FEB, 15); - - var ngElement, element, scope, pageScope, controller; - var $compile, $timeout, $$rAF, $animate, $window, keyCodes, dateUtil, dateLocale; - - var DATEPICKER_TEMPLATE = - '' + - ''; - beforeEach(module('material.components.datepicker', 'material.components.input', 'ngAnimateMock')); beforeEach(inject(function($rootScope, $injector) { @@ -51,31 +75,6 @@ describe('md-datepicker', function() { ngElement.remove(); }); - /** - * Compile and link the given template and store values for element, scope, and controller. - * @param {string} template - * @returns {angular.JQLite} The root compiled element. - */ - function createDatepickerInstance(template) { - var outputElement = $compile(template)(pageScope); - pageScope.$apply(); - - ngElement = outputElement[0].tagName == 'MD-DATEPICKER' ? - outputElement : outputElement.find('md-datepicker'); - element = ngElement[0]; - scope = ngElement.isolateScope(); - controller = ngElement.controller('mdDatepicker'); - - return outputElement; - } - - /** Populates the inputElement with a value and triggers the input events. */ - function populateInputElement(inputString) { - controller.ngInputElement.val(inputString).triggerHandler('input'); - $timeout.flush(); - pageScope.$apply(); - } - it('should be the same date object as the initial ng-model', function() { expect(pageScope.myDate).toBe(initialDate); }); @@ -591,9 +590,9 @@ describe('md-datepicker', function() { body.removeChild(element); }); - it('should shink the calendar pane when it would otherwise not fit on the screen', function() { + it('should shrink the calendar pane when it would otherwise not fit on the screen', function() { // Fake the window being very narrow so that the calendar pane won't fit on-screen. - controller.$window = {innerWidth: 200, innherHeight: 800}; + controller.$window = {innerWidth: 200, innerHeight: 800}; // Open the calendar pane. controller.openCalendarPane({}); @@ -893,3 +892,57 @@ describe('md-datepicker', function() { }); }); + +describe('md-datepicker with MomentJS custom formatting', function() { + beforeEach(module('material.components.datepicker', 'material.components.input', 'ngAnimateMock')); + + beforeEach(module(function($mdDateLocaleProvider) { + $mdDateLocaleProvider.formatDate = function(date) { + return date ? moment(date).format('M/D') : ''; + }; + $mdDateLocaleProvider.parseDate = function(dateString) { + var m = moment(dateString, 'M/D', true); + return m.isValid() ? m.toDate() : new Date(NaN); + }; + $mdDateLocaleProvider.isDateComplete = function(dateString) { + dateString = dateString.trim(); + // Look for two chunks of content (either numbers or text) separated by delimiters. + var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/; + return re.test(dateString); + }; + })); + + beforeEach(inject(function($rootScope, $injector) { + $compile = $injector.get('$compile'); + $timeout = $injector.get('$timeout'); + + pageScope = $rootScope.$new(); + pageScope.myDate = initialDate; + pageScope.isDisabled = false; + pageScope.dateChangedHandler = jasmine.createSpy('ng-change handler'); + + createDatepickerInstance(DATEPICKER_TEMPLATE); + controller.closeCalendarPane(); + })); + + afterEach(function() { + controller.isAttached && controller.closeCalendarPane(); + pageScope.$destroy(); + ngElement.remove(); + }); + + it('should update the model value and close the calendar pane', function() { + var date = new Date(2020, SEP, 1); + controller.openCalendarPane({ + target: controller.inputElement + }); + scope.$emit('md-calendar-change', date); + scope.$apply(); + expect(pageScope.myDate).toEqual(date); + expect(controller.ngModelCtrl.$modelValue).toEqual(date); + + expect(controller.inputElement.value).toEqual('9/1'); + expect(controller.calendarPaneOpenedFrom).toBe(null); + expect(controller.isCalendarOpen).toBe(false); + }); +});