Skip to content

Commit 7c87628

Browse files
committed
test(ngOptions): fix flaky test on Firefox 54+ and Safari 9
Closes angular#16149
1 parent a83d646 commit 7c87628

File tree

1 file changed

+128
-75
lines changed

1 file changed

+128
-75
lines changed

test/ng/directive/ngOptionsSpec.js

+128-75
Original file line numberDiff line numberDiff line change
@@ -2922,84 +2922,137 @@ describe('ngOptions', function() {
29222922
});
29232923

29242924

2925-
it('should not re-set the `selected` property if it already has the correct value', function() {
2926-
scope.values = [{name: 'A'}, {name: 'B'}];
2927-
createMultiSelect();
2925+
// Support: Safari 9
2926+
// This test relies defining a getter/setter `selected` property on either `<option>` elements
2927+
// or their prototype. Some browsers (including Safari 9) are very flakey when the
2928+
// getter/setter is not defined on the prototype (probably due to some bug). On Safari 9, the
2929+
// getter/setter that is already defined on the `<option>` element's prototype is not
2930+
// configurable, so we can't overwrite it with our spy.
2931+
if (!/\b9(?:\.\d+)+ safari/i.test(window.navigator.userAgent)) {
2932+
it('should not re-set the `selected` property if it already has the correct value', function() {
2933+
scope.values = [{name: 'A'}, {name: 'B'}];
2934+
createMultiSelect();
29282935

2929-
var options = element.find('option');
2930-
var optionsSetSelected = [];
2931-
var _selected = [];
2932-
2933-
// Set up spies
2934-
forEach(options, function(option, i) {
2935-
optionsSetSelected[i] = jasmine.createSpy('optionSetSelected' + i);
2936-
_selected[i] = option.selected;
2937-
Object.defineProperty(option, 'selected', {
2938-
get: function() { return _selected[i]; },
2939-
set: optionsSetSelected[i].and.callFake(function(value) { _selected[i] = value; })
2936+
var options = element.find('option');
2937+
var optionsSetSelected = [];
2938+
var _selected = [];
2939+
2940+
// Set up spies
2941+
var optionProto = Object.getPrototypeOf(options[0]);
2942+
var originalSelectedDescriptor = isFunction(Object.getOwnPropertyDescriptor) &&
2943+
Object.getOwnPropertyDescriptor(optionProto, 'selected');
2944+
var addSpiesOnProto = originalSelectedDescriptor && originalSelectedDescriptor.configurable;
2945+
2946+
forEach(options, function(option, i) {
2947+
var setSelected = function(value) { _selected[i] = value; };
2948+
optionsSetSelected[i] = jasmine.createSpy('optionSetSelected' + i).and.callFake(setSelected);
2949+
setSelected(option.selected);
29402950
});
2941-
});
29422951

2943-
// Select `optionA`
2944-
scope.$apply('selected = [values[0]]');
2945-
2946-
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
2947-
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
2948-
expect(options[0].selected).toBe(true);
2949-
expect(options[1].selected).toBe(false);
2950-
optionsSetSelected[0].calls.reset();
2951-
optionsSetSelected[1].calls.reset();
2952-
2953-
// Select `optionB` (`optionA` remains selected)
2954-
scope.$apply('selected.push(values[1])');
2955-
2956-
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
2957-
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(true);
2958-
expect(options[0].selected).toBe(true);
2959-
expect(options[1].selected).toBe(true);
2960-
optionsSetSelected[0].calls.reset();
2961-
optionsSetSelected[1].calls.reset();
2962-
2963-
// Unselect `optionA` (`optionB` remains selected)
2964-
scope.$apply('selected.shift()');
2965-
2966-
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
2967-
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
2968-
expect(options[0].selected).toBe(false);
2969-
expect(options[1].selected).toBe(true);
2970-
optionsSetSelected[0].calls.reset();
2971-
optionsSetSelected[1].calls.reset();
2972-
2973-
// Reselect `optionA` (`optionB` remains selected)
2974-
scope.$apply('selected.push(values[0])');
2975-
2976-
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
2977-
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
2978-
expect(options[0].selected).toBe(true);
2979-
expect(options[1].selected).toBe(true);
2980-
optionsSetSelected[0].calls.reset();
2981-
optionsSetSelected[1].calls.reset();
2982-
2983-
// Unselect `optionB` (`optionA` remains selected)
2984-
scope.$apply('selected.shift()');
2985-
2986-
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
2987-
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(false);
2988-
expect(options[0].selected).toBe(true);
2989-
expect(options[1].selected).toBe(false);
2990-
optionsSetSelected[0].calls.reset();
2991-
optionsSetSelected[1].calls.reset();
2992-
2993-
// Unselect `optionA`
2994-
scope.$apply('selected.length = 0');
2995-
2996-
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
2997-
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
2998-
expect(options[0].selected).toBe(false);
2999-
expect(options[1].selected).toBe(false);
3000-
optionsSetSelected[0].calls.reset();
3001-
optionsSetSelected[1].calls.reset();
3002-
});
2952+
if (!addSpiesOnProto) {
2953+
forEach(options, function(option, i) {
2954+
Object.defineProperty(option, 'selected', {
2955+
get: function() { return _selected[i]; },
2956+
set: optionsSetSelected[i]
2957+
});
2958+
});
2959+
} else {
2960+
// Support: Firefox 54+
2961+
// We cannot use the above (simpler) method on all browsers because of Firefox 54+, which
2962+
// is very flaky when the getter/setter property is defined on the element itself and not
2963+
// the prototype. (Possibly the result of some (buggy?) optimization.)
2964+
var getSelected = function(index) { return _selected[index]; };
2965+
var setSelected = function(index, value) { optionsSetSelected[index](value); };
2966+
var getSelectedOriginal = function(option) {
2967+
return originalSelectedDescriptor.get.call(option);
2968+
};
2969+
var setSelectedOriginal = function(option, value) {
2970+
originalSelectedDescriptor.set.call(option, value);
2971+
};
2972+
var getIndexAndCall = function(option, foundFn, notFoundFn, value) {
2973+
for (var i = 0, ii = options.length; i < ii; ++i) {
2974+
if (options[i] === option) return foundFn(i, value);
2975+
}
2976+
return notFoundFn(option, value);
2977+
};
2978+
2979+
Object.defineProperty(optionProto, 'selected', {
2980+
get: function() {
2981+
return getIndexAndCall(this, getSelected, getSelectedOriginal);
2982+
},
2983+
set: function(value) {
2984+
return getIndexAndCall(this, setSelected, setSelectedOriginal, value);
2985+
}
2986+
});
2987+
}
2988+
2989+
// Select `optionA`
2990+
scope.$apply('selected = [values[0]]');
2991+
2992+
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
2993+
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
2994+
expect(options[0].selected).toBe(true);
2995+
expect(options[1].selected).toBe(false);
2996+
optionsSetSelected[0].calls.reset();
2997+
optionsSetSelected[1].calls.reset();
2998+
2999+
// Select `optionB` (`optionA` remains selected)
3000+
scope.$apply('selected.push(values[1])');
3001+
3002+
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
3003+
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(true);
3004+
expect(options[0].selected).toBe(true);
3005+
expect(options[1].selected).toBe(true);
3006+
optionsSetSelected[0].calls.reset();
3007+
optionsSetSelected[1].calls.reset();
3008+
3009+
// Unselect `optionA` (`optionB` remains selected)
3010+
scope.$apply('selected.shift()');
3011+
3012+
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
3013+
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3014+
expect(options[0].selected).toBe(false);
3015+
expect(options[1].selected).toBe(true);
3016+
optionsSetSelected[0].calls.reset();
3017+
optionsSetSelected[1].calls.reset();
3018+
3019+
// Reselect `optionA` (`optionB` remains selected)
3020+
scope.$apply('selected.push(values[0])');
3021+
3022+
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
3023+
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3024+
expect(options[0].selected).toBe(true);
3025+
expect(options[1].selected).toBe(true);
3026+
optionsSetSelected[0].calls.reset();
3027+
optionsSetSelected[1].calls.reset();
3028+
3029+
// Unselect `optionB` (`optionA` remains selected)
3030+
scope.$apply('selected.shift()');
3031+
3032+
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
3033+
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(false);
3034+
expect(options[0].selected).toBe(true);
3035+
expect(options[1].selected).toBe(false);
3036+
optionsSetSelected[0].calls.reset();
3037+
optionsSetSelected[1].calls.reset();
3038+
3039+
// Unselect `optionA`
3040+
scope.$apply('selected.length = 0');
3041+
3042+
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
3043+
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3044+
expect(options[0].selected).toBe(false);
3045+
expect(options[1].selected).toBe(false);
3046+
optionsSetSelected[0].calls.reset();
3047+
optionsSetSelected[1].calls.reset();
3048+
3049+
// Support: Firefox 54+
3050+
// Restore `originalSelectedDescriptor`
3051+
if (addSpiesOnProto) {
3052+
Object.defineProperty(optionProto, 'selected', originalSelectedDescriptor);
3053+
}
3054+
});
3055+
}
30033056

30043057
if (window.MutationObserver) {
30053058
//IE9 and IE10 do not support MutationObserver

0 commit comments

Comments
 (0)