From 080357e906e2ec34e669091ef345fc4442e23ea0 Mon Sep 17 00:00:00 2001 From: Jacob Hansson Date: Fri, 21 Apr 2017 06:39:36 -0500 Subject: [PATCH 001/618] feat(ngMock): describe unflushed http requests The current implementation of $httpBackend.verifyNoOutstandingRequest gives an integer number describing how many requests are unflushed. While it's superficially easy to solve test errors from that message by simply adding an additional $httpBackend.flush(), if a developer is truly not expecting the code to make further requests this is not ideal. This change explicitly prints out which additional requests remain unflushed in the error message, helping her determine if the code needs changing, or if an additional flush is appropriate. Before this change: Unflushed requests: 1 After this change: Unflushed requests: 1 GET /some Closes #10596 Closes #15928 --- src/ngMock/angular-mocks.js | 5 ++++- test/ngMock/angular-mocksSpec.js | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index d6e84c75584f..1d21364da7bb 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1378,6 +1378,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } } + handleResponse.description = method + ' ' + url; return handleResponse; function handleResponse() { @@ -1884,7 +1885,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { $httpBackend.verifyNoOutstandingRequest = function(digest) { if (digest !== false) $rootScope.$digest(); if (responses.length) { - throw new Error('Unflushed requests: ' + responses.length); + var unflushedDescriptions = responses.map(function(res) { return res.description; }); + throw new Error('Unflushed requests: ' + responses.length + '\n ' + + unflushedDescriptions.join('\n ')); } }; diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 441509376561..4bfad9d3bb10 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1678,7 +1678,8 @@ describe('ngMock', function() { expect(function() { hb.verifyNoOutstandingRequest(); - }).toThrowError('Unflushed requests: 1'); + }).toThrowError('Unflushed requests: 1\n' + + ' GET /some'); }); @@ -1690,8 +1691,23 @@ describe('ngMock', function() { expect(function() { hb.verifyNoOutstandingRequest(); - }).toThrowError('Unflushed requests: 1'); + }).toThrowError('Unflushed requests: 1\n' + + ' GET /some'); })); + + + it('should describe multiple unflushed requests', function() { + hb.when('GET').respond(200); + hb.when('PUT').respond(200); + hb('GET', '/some', null, noop, {}); + hb('PUT', '/elsewhere', null, noop, {}); + + expect(function() { + hb.verifyNoOutstandingRequest(); + }).toThrowError('Unflushed requests: 2\n' + + ' GET /some\n' + + ' PUT /elsewhere'); + }); }); From 8d7c7f4a8eed3dbf46ecb277c54b5c0f1eb1958e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 9 Aug 2016 22:06:55 +0200 Subject: [PATCH 002/618] test(select, ngOptions): add more tests for "required" with "empty" or "unknown" option --- src/ng/directive/ngOptions.js | 1 - test/ng/directive/ngOptionsSpec.js | 127 +++++++++++++++++++---- test/ng/directive/selectSpec.js | 157 ++++++++++++++++++++++------- 3 files changed, 231 insertions(+), 54 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index e82d5e49813a..0d4323487cae 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -704,7 +704,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, ngModelCtrl.$render(); } } - } } diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js index 7a3c4ba492bf..4aae08cabcf0 100644 --- a/test/ng/directive/ngOptionsSpec.js +++ b/test/ng/directive/ngOptionsSpec.js @@ -2,12 +2,13 @@ describe('ngOptions', function() { - var scope, formElement, element, $compile, linkLog; + var scope, formElement, element, $compile, linkLog, ngModelCtrl; function compile(html) { formElement = jqLite('
' + html + '
'); element = formElement.find('select'); $compile(formElement)(scope); + ngModelCtrl = element.controller('ngModel'); scope.$apply(); } @@ -181,6 +182,7 @@ describe('ngOptions', function() { afterEach(function() { scope.$destroy(); //disables unknown option work during destruction dealoc(formElement); + ngModelCtrl = null; }); function createSelect(attrs, blank, unknown) { @@ -2925,42 +2927,68 @@ describe('ngOptions', function() { }); - describe('ngRequired', function() { + describe('required state', function() { - it('should allow bindings on ngRequired', function() { + it('should set the error if the empty option is selected', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }, true); + + scope.$apply(function() { + scope.values = ['a', 'b']; + scope.selection = scope.values[0]; + }); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + var options = element.find('option'); + + // view -> model + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + + browserTrigger(options[1], 'click'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + // model -> view + scope.$apply('selection = "unmatched value"'); + expect(options[0]).toBeMarkedAsSelected(); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + }); + + + it('should validate with empty option and bound ngRequired', function() { createSelect({ 'ng-model': 'value', 'ng-options': 'item.name for item in values', 'ng-required': 'required' }, true); - scope.$apply(function() { scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; scope.required = false; }); - element.val(''); - browserTrigger(element, 'change'); + var options = element.find('option'); + + browserTrigger(options[0], 'click'); expect(element).toBeValid(); - scope.$apply(function() { - scope.required = true; - }); + scope.$apply('required = true'); expect(element).toBeInvalid(); - scope.$apply(function() { - scope.value = scope.values[0]; - }); + scope.$apply('value = values[0]'); expect(element).toBeValid(); - element.val(''); - browserTrigger(element, 'change'); + browserTrigger(options[0], 'click'); expect(element).toBeInvalid(); - scope.$apply(function() { - scope.required = false; - }); + scope.$apply('required = false'); expect(element).toBeValid(); }); @@ -2989,6 +3017,43 @@ describe('ngOptions', function() { }); + it('should NOT set the error if the empty option is present but required attribute is not', + function() { + scope.$apply(function() { + scope.values = ['a', 'b']; + }); + + createSingleSelect(); + + expect(element).toBeValid(); + expect(element).toBePristine(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + } + ); + + + it('should NOT set the error if the unknown option is selected', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }); + + scope.$apply(function() { + scope.values = ['a', 'b']; + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply('selection = "c"'); + expect(element).toEqualSelect(['?'], 'string:a', 'string:b'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + }); + + it('should allow falsy values as values', function() { createSelect({ 'ng-model': 'value', @@ -3009,6 +3074,34 @@ describe('ngOptions', function() { expect(element).toBeValid(); expect(scope.value).toBe(false); }); + + + it('should validate after option list was updated', function() { + createSelect({ + 'ng-model': 'selection', + 'ng-options': 'item for item in values', + 'required': '' + }, true); + + scope.$apply(function() { + scope.values = ['A', 'B']; + scope.selection = scope.values[0]; + }); + + expect(element).toEqualSelect('', ['string:A'], 'string:B'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply(function() { + scope.values = ['C', 'D']; + }); + + expect(element).toEqualSelect([''], 'string:C', 'string:D'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + // ngModel sets undefined for invalid values + expect(scope.selection).toBeUndefined(); + }); }); describe('required and empty option', function() { diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 975e4b594d77..1a372f772973 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -7,6 +7,7 @@ describe('select', function() { formElement = jqLite('
' + html + '
'); element = formElement.find('select'); $compile(formElement)(scope); + ngModelCtrl = element.controller('ngModel'); scope.$digest(); } @@ -79,6 +80,7 @@ describe('select', function() { afterEach(function() { scope.$destroy(); //disables unknown option work during destruction dealoc(formElement); + ngModelCtrl = null; }); @@ -190,54 +192,108 @@ describe('select', function() { }); - it('should require', function() { - compile( - ''); + describe('required state', function() { - scope.change = function() { - scope.log += 'change;'; - }; + it('should set the error if the empty option is selected', function() { + compile( + ''); - scope.$apply(function() { - scope.log = ''; - scope.selection = 'c'; + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + var options = element.find('option'); + + // view -> model + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + + browserTrigger(options[1], 'click'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + // model -> view + scope.$apply('selection = null'); + options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); }); - expect(scope.form.select.$error.required).toBeFalsy(); - expect(element).toBeValid(); - expect(element).toBePristine(); - scope.$apply(function() { - scope.selection = ''; + it('should validate with empty option and bound ngRequired', function() { + compile( + ''); + + scope.$apply(function() { + scope.required = false; + }); + + var options = element.find('option'); + + browserTrigger(options[0], 'click'); + expect(element).toBeValid(); + + scope.$apply('required = true'); + expect(element).toBeInvalid(); + + scope.$apply('selection = "a"'); + expect(element).toBeValid(); + expect(element).toEqualSelect('', ['a'], 'b'); + + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + + scope.$apply('required = false'); + expect(element).toBeValid(); }); - expect(scope.form.select.$error.required).toBeTruthy(); - expect(element).toBeInvalid(); - expect(element).toBePristine(); - expect(scope.log).toEqual(''); - element[0].value = 'c'; - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(element).toBeDirty(); - expect(scope.log).toEqual('change;'); - }); + it('should not be invalid if no required attribute is present', function() { + compile( + ''); + expect(element).toBeValid(); + expect(element).toBePristine(); + }); - it('should not be invalid if no require', function() { - compile( - ''); - expect(element).toBeValid(); - expect(element).toBePristine(); - }); + it('should NOT set the error if the unknown option is selected', function() { + compile( + ''); + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply('selection = "c"'); + expect(element).toEqualSelect([unknownValue('c')], 'a', 'b'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + }); + + }); it('should work with repeated value options', function() { scope.robots = ['c3p0', 'r2d2']; @@ -2358,6 +2414,35 @@ describe('select', function() { expect(previouslySelectedOptionElement).not.toBe(optionElements[0]); }); + + it('should validate when the options change', function() { + scope.values = ['A', 'B']; + scope.selection = 'A'; + + compile( + '' + ); + + expect(element).toEqualSelect('', ['A'], 'B'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply(function() { + // Only when new objects are used, ngRepeat re-creates the element from scratch + scope.values = ['B', 'C']; + }); + + expect(element).toEqualSelect([''], 'B', 'C'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + // ngModel sets undefined for invalid values + expect(scope.selection).toBeUndefined(); + }); + + }); From 5878f07474755cb3df1e727cef4e7e4716f44783 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 15:24:33 +0200 Subject: [PATCH 003/618] fix(ngOptions): select unknown option if unmatched model does not match empty option When a regular / ngOptions select has an explicit *empty* option, this option can be selected by the user and will set the model to `null`. It is also selected when the model is set to `null` or `undefined`. When the model is set to a value that does not match any option value, and is also not `null` or `undefined`, the *unknown* option is inserted and selected - this is an explicit marker that the select is in an invalid / unknown state, which is different from an allowed empty state. Previously, regular selects followed this logic, whereas ngOptions selects selected the empty option in the case described above. This patch makes the behavior consistent between regular / ngOptions select - the latter will now insert and select the unknown option. The order of the options has been fixed to unknown -> empty -> actual options. --- src/ng/directive/ngOptions.js | 10 ++++-- src/ng/directive/select.js | 12 ++++--- test/helpers/matchers.js | 21 ++++++++++++ test/ng/directive/ngOptionsSpec.js | 51 ++++++++++++++++++++++++++-- test/ng/directive/selectSpec.js | 54 +++++++++++++++--------------- 5 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 0d4323487cae..97b4507fd871 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -473,7 +473,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - if (providedEmptyOption) { + if (value == null && providedEmptyOption) { + selectCtrl.removeUnknownOption(); selectCtrl.selectEmptyOption(); } else if (selectCtrl.unknownOption.parent().length) { selectCtrl.updateUnknownOption(value); @@ -657,7 +658,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // Ensure that the empty option is always there if it was explicitly provided if (providedEmptyOption) { - selectElement.prepend(selectCtrl.emptyOption); + + if (selectCtrl.unknownOption.parent().length) { + selectCtrl.unknownOption.after(selectCtrl.emptyOption); + } else { + selectElement.prepend(selectCtrl.emptyOption); + } } options.items.forEach(function addOption(option) { diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index dc828764d3da..1f9d0cba0e7f 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -44,11 +44,13 @@ var SelectController = // to create it in ' + + '' + '' + '' + ''); @@ -411,24 +390,45 @@ describe('select', function() { scope.$digest(); options = element.find('option'); - expect(options.length).toBe(2); - expect(options[0]).toBeMarkedAsSelected(); - expect(options[1]).not.toBeMarkedAsSelected(); + expect(options.length).toBe(3); + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'b'; scope.$digest(); options = element.find('option'); expect(options[0]).not.toBeMarkedAsSelected(); - expect(options[1]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); - scope.selected = 'no match'; + // This will select the empty option + scope.selected = null; scope.$digest(); + expect(options[0]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); + + // This will add and select the unknown option + scope.selected = 'unmatched value'; + scope.$digest(); options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); + expect(options[3]).not.toBeMarkedAsSelected(); + + // Back to matched value + scope.selected = 'b'; + scope.$digest(); + options = element.find('option'); + + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); }); describe('empty option', function() { From 4b06637f703d2a94baedfda64a8e3ac8eea26403 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 15:16:15 +0200 Subject: [PATCH 004/618] chore(matchers): improve output for toBeMarkedAsSelected --- test/helpers/matchers.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index c543bbb965aa..ac297609e579 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -365,13 +365,15 @@ beforeEach(function() { return { compare: function(actual) { var errors = []; + var optionVal = toJson(actual.value); + if (actual.selected === null || typeof actual.selected === 'undefined' || actual.selected === false) { - errors.push('Expected option property "selected" to be truthy'); + errors.push('Expected option with value ' + optionVal + ' to have property "selected" set to truthy'); } // Support: IE 9 only if (msie !== 9 && actual.hasAttribute('selected') === false) { - errors.push('Expected option to have attribute "selected"'); + errors.push('Expected option with value ' + optionVal + ' to have attribute "selected"'); } var result = { @@ -383,13 +385,15 @@ beforeEach(function() { }, negativeCompare: function(actual) { var errors = []; + var optionVal = toJson(actual.value); + if (actual.selected) { - errors.push('Expected option property "selected" to be falsy'); + errors.push('Expected option with value ' + optionVal + ' property "selected" to be falsy'); } // Support: IE 9 only if (msie !== 9 && actual.hasAttribute('selected')) { - errors.push('Expected option not to have attribute "selected"'); + errors.push('Expected option with value ' + optionVal + ' not to have attribute "selected"'); } var result = { From ff0e61166d3dca59351e3913e0360c24d1bce99c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 18 Apr 2017 19:59:42 +0200 Subject: [PATCH 005/618] refactor(select, ngOptions): extract common methods; make consistent --- src/ng/directive/ngOptions.js | 15 +++------------ src/ng/directive/select.js | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 97b4507fd871..eecc6ccaa8c6 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -449,12 +449,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (!multiple) { selectCtrl.writeValue = function writeNgOptionsValue(value) { - var selectedOption = options.selectValueMap[selectElement.val()]; + var selectedOption = selectElement[0].options[selectElement[0].selectedIndex]; var option = options.getOptionFromViewValue(value); // Make sure to remove the selected attribute from the previously selected option // Otherwise, screen readers might get confused - if (selectedOption) selectedOption.element.removeAttribute('selected'); + if (selectedOption) selectedOption.removeAttribute('selected'); if (option) { // Don't update the option when it is already selected. @@ -464,7 +464,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (selectElement[0].value !== option.selectValue) { selectCtrl.removeUnknownOption(); - selectCtrl.unselectEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; @@ -472,15 +471,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - - if (value == null && providedEmptyOption) { - selectCtrl.removeUnknownOption(); - selectCtrl.selectEmptyOption(); - } else if (selectCtrl.unknownOption.parent().length) { - selectCtrl.updateUnknownOption(value); - } else { - selectCtrl.renderUnknownOption(value); - } + selectCtrl.selectUnknownOrEmptyOption(value); } }; diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 1f9d0cba0e7f..d8a19605f96d 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -86,7 +86,7 @@ var SelectController = self.unselectEmptyOption = function() { if (self.hasEmptyOption) { - self.emptyOption.removeAttr('selected'); + setOptionSelectedStatus(self.emptyOption, false); } }; @@ -128,14 +128,7 @@ var SelectController = var selectedOption = $element[0].options[$element[0].selectedIndex]; setOptionSelectedStatus(jqLite(selectedOption), true); } else { - if (value == null && self.emptyOption) { - self.removeUnknownOption(); - self.selectEmptyOption(); - } else if (self.unknownOption.parent().length) { - self.updateUnknownOption(value); - } else { - self.renderUnknownOption(value); - } + self.selectUnknownOrEmptyOption(value); } }; @@ -178,6 +171,16 @@ var SelectController = return !!optionsMap.get(value); }; + self.selectUnknownOrEmptyOption = function(value) { + if (value == null && self.emptyOption) { + self.removeUnknownOption(); + self.selectEmptyOption(); + } else if (self.unknownOption.parent().length) { + self.updateUnknownOption(value); + } else { + self.renderUnknownOption(value); + } + }; var renderScheduled = false; function scheduleRender() { From e4c2fe6d427cb1540977520f7e31a7e7a30acfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Apr 2017 13:19:53 +0200 Subject: [PATCH 006/618] refactor(*): remove workarounds for IE <9, update IE/Edge-related comments --- .github/ISSUE_TEMPLATE.md | 4 +- .../ngdoc/api/directive.template.html | 3 -- docs/content/guide/bootstrap.ngdoc | 4 +- docs/content/guide/ie.ngdoc | 8 ++-- docs/content/misc/faq.ngdoc | 6 +-- src/Angular.js | 10 ++-- src/ng/directive/ngOptions.js | 6 ++- src/ng/directive/select.js | 8 ++-- src/ngSanitize/sanitize.js | 16 ++----- test/AngularSpec.js | 1 + test/helpers/privateMocks.js | 4 +- test/helpers/testabilityPatch.js | 21 +------- test/jqLiteSpec.js | 2 +- test/minErrSpec.js | 1 + test/ng/compileSpec.js | 48 ++++++++----------- test/ng/directive/booleanAttrsSpec.js | 2 +- test/ng/locationSpec.js | 28 ++--------- test/ng/urlUtilsSpec.js | 2 +- 18 files changed, 57 insertions(+), 117 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 82e6b016d1cb..5e11e611ee8e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -29,8 +29,8 @@ https://plnkr.co or similar (you can use this template as a starting point: http **Angular version:** 1.x.y -**Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] +**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] **Anything else:** - \ No newline at end of file + diff --git a/docs/config/templates/ngdoc/api/directive.template.html b/docs/config/templates/ngdoc/api/directive.template.html index 7e14ce0c6411..b30bdb0451f2 100644 --- a/docs/config/templates/ngdoc/api/directive.template.html +++ b/docs/config/templates/ngdoc/api/directive.template.html @@ -18,9 +18,6 @@

Usage

    {% if doc.restrict.element %}
  • as element: - {% if doc.name.indexOf('ng') == 0 -%} - (This directive can be used as custom element, but be aware of IE restrictions). - {%- endif %} {% code %} <{$ doc.name | dashCase $} {%- for param in doc.params %} diff --git a/docs/content/guide/bootstrap.ngdoc b/docs/content/guide/bootstrap.ngdoc index bd13ec1b1d8e..573a989818e6 100644 --- a/docs/content/guide/bootstrap.ngdoc +++ b/docs/content/guide/bootstrap.ngdoc @@ -40,8 +40,8 @@ initialization. 3. If you choose to use the old style directive syntax `ng:` then include xml-namespace in `html` - to make IE happy. (This is here for historical reasons, and we no longer recommend use of - `ng:`.) + when running the page in the XHTML mode. (This is here for historical reasons, and we no longer + recommend use of `ng:`.) diff --git a/docs/content/guide/ie.ngdoc b/docs/content/guide/ie.ngdoc index 8cc554e8ec01..ffd70ba69b4c 100644 --- a/docs/content/guide/ie.ngdoc +++ b/docs/content/guide/ie.ngdoc @@ -7,7 +7,7 @@
    **Note:** AngularJS 1.3 has dropped support for IE8. Read more about it on -[our blog](http://blog.angularjs.org/2013/12/angularjs-13-new-release-approaches.html). +[our blog](https://blog.angularjs.org/2013/12/angularjs-13-new-release-approaches.html). AngularJS 1.2 will continue to support IE8, but the core team does not plan to spend time addressing issues specific to IE8 or earlier.
    @@ -19,7 +19,7 @@ on IE. The project currently supports and will attempt to fix bugs for IE9 and above. The continuous integration server runs all the tests against IE9, IE10, and IE11. See [Travis CI](https://travis-ci.org/angular/angular.js) and -[ci.angularjs.org](http://ci.angularjs.org). +[ci.angularjs.org](https://ci.angularjs.org). We do not run tests on IE8 and below. A subset of the AngularJS functionality may work on these browsers, but it is up to you to test and decide whether it works for your particular app. @@ -27,8 +27,8 @@ browsers, but it is up to you to test and decide whether it works for your parti To ensure your AngularJS application works on IE please consider: -1. Use `ng-style` tags instead of `style="{{ someCss }}"`. The latter works in Chrome and Firefox - but does not work in Internet Explorer <= 11 (the most recent version at time of writing). +1. Use `ng-style` tags instead of `style="{{ someCss }}"`. The latter works in Chrome, Firefox, + Safari and Edge but does not work in Internet Explorer (even 11). 2. For the `type` attribute of buttons, use `ng-attr-type` tags instead of `type="{{ someExpression }}"`. If using the latter, Internet Explorer overwrites the expression with `type="submit"` before AngularJS has a chance to interpolate it. diff --git a/docs/content/misc/faq.ngdoc b/docs/content/misc/faq.ngdoc index cb3838f6e219..e7029067a90d 100644 --- a/docs/content/misc/faq.ngdoc +++ b/docs/content/misc/faq.ngdoc @@ -142,10 +142,8 @@ We run our extensive test suite against the following browsers: the latest versi Firefox, Safari, and Safari for iOS, as well as Internet Explorer versions 9-11. See {@link guide/ie Internet Explorer Compatibility} for more details on supporting legacy IE browsers. -If a browser is untested, it doesn't mean it won't work; for example, older Android (2.3.x) -is supported in the sense that we avoid the dot notation for reserved words as property names, -but we don't actively test changes against it. You can also expect browsers to work that share -a large part of their codebase with a browser we test, such as Opera > version 12 +If a browser is untested, it doesn't mean it won't work. You can also expect browsers to work that +share a large part of their codebase with a browser we test, such as Opera 15 or newer (uses the Blink engine), or the various Firefox derivatives. diff --git a/src/Angular.js b/src/Angular.js index 7f539e81fb52..42942d432c01 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1353,7 +1353,7 @@ function fromJson(json) { var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { - // Support: IE 9-11 only, Edge 13-14+ + // Support: IE 9-11 only, Edge 13-15+ // IE/Edge do not "understand" colon (`:`) in timezone timezone = timezone.replace(ALL_COLONS, ''); var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; @@ -1380,12 +1380,7 @@ function convertTimezoneToLocal(date, timezone, reverse) { * @returns {string} Returns the string representation of the element. */ function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch (e) { /* empty */ } + element = jqLite(element).clone().empty(); var elemHtml = jqLite('
    ').append(element).html(); try { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : @@ -1523,6 +1518,7 @@ function allowAutoBootstrap(document) { var script = document.currentScript; if (!script) { + // Support: IE 9-11 only // IE does not have `document.currentScript` return true; } diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index eecc6ccaa8c6..91cc45bf16aa 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -407,7 +407,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, } - // we can't just jqLite('