@@ -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 ( ! / \b 9 (?: \. \d + ) + s a f a r i / 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