313 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			313 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * @license | ||
|  |  * Copyright Google Inc. All Rights Reserved. | ||
|  |  * | ||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||
|  |  */ | ||
|  | 
 | ||
|  | import {ifEnvSupports} from '../test-util'; | ||
|  | 
 | ||
|  | describe('element', function() { | ||
|  |   let button: HTMLButtonElement; | ||
|  | 
 | ||
|  |   beforeEach(function() { | ||
|  |     button = document.createElement('button'); | ||
|  |     document.body.appendChild(button); | ||
|  |   }); | ||
|  | 
 | ||
|  |   afterEach(function() { document.body.removeChild(button); }); | ||
|  | 
 | ||
|  |   // https://github.com/angular/zone.js/issues/190
 | ||
|  |   it('should work when addEventListener / removeEventListener are called in the global context', | ||
|  |      function() { | ||
|  |        const clickEvent = document.createEvent('Event'); | ||
|  |        let callCount = 0; | ||
|  | 
 | ||
|  |        clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |        const listener = function(event: Event) { | ||
|  |          callCount++; | ||
|  |          expect(event).toBe(clickEvent); | ||
|  |        }; | ||
|  | 
 | ||
|  |        // `this` would be null inside the method when `addEventListener` is called from strict mode
 | ||
|  |        // it would be `window`:
 | ||
|  |        // - when called from non strict-mode,
 | ||
|  |        // - when `window.addEventListener` is called explicitly.
 | ||
|  |        addEventListener('click', listener); | ||
|  | 
 | ||
|  |        button.dispatchEvent(clickEvent); | ||
|  |        expect(callCount).toEqual(1); | ||
|  | 
 | ||
|  |        removeEventListener('click', listener); | ||
|  |        button.dispatchEvent(clickEvent); | ||
|  |        expect(callCount).toEqual(1); | ||
|  |      }); | ||
|  | 
 | ||
|  |   it('should work with addEventListener when called with a function listener', function() { | ||
|  |     const clickEvent = document.createEvent('Event'); | ||
|  |     clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |     button.addEventListener('click', function(event) { expect(event).toBe(clickEvent as any); }); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should not call microtasks early when an event is invoked', function(done) { | ||
|  |     let log = ''; | ||
|  |     button.addEventListener('click', () => { | ||
|  |       Zone.current.scheduleMicroTask('test', () => log += 'microtask;'); | ||
|  |       log += 'click;'; | ||
|  |     }); | ||
|  |     button.click(); | ||
|  | 
 | ||
|  |     expect(log).toEqual('click;'); | ||
|  |     done(); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should call microtasks early when an event is invoked', function(done) { | ||
|  |     /* | ||
|  |      * In this test we escape the Zone using unpatched setTimeout. | ||
|  |      * This way the eventTask invoked from click will think it is the top most | ||
|  |      * task and eagerly drain the microtask queue. | ||
|  |      * | ||
|  |      * THIS IS THE WRONG BEHAVIOR! | ||
|  |      * | ||
|  |      * But there is no easy way for the task to know if it is the top most task. | ||
|  |      * | ||
|  |      * Given that this can only arise when someone is emulating clicks on DOM in a synchronous | ||
|  |      * fashion we have few choices: | ||
|  |      * 1. Ignore as this is unlikely to be a problem outside of tests. | ||
|  |      * 2. Monkey patch the event methods to increment the _numberOfNestedTaskFrames and prevent | ||
|  |      *    eager drainage. | ||
|  |      * 3. Pay the cost of throwing an exception in event tasks and verifying that we are the | ||
|  |      *    top most frame. | ||
|  |      * | ||
|  |      * For now we are choosing to ignore it and assume that this arises in tests only. | ||
|  |      * As an added measure we make sure that all jasmine tests always run in a task. See: jasmine.ts | ||
|  |      */ | ||
|  |     (window as any)[(Zone as any).__symbol__('setTimeout')](() => { | ||
|  |       let log = ''; | ||
|  |       button.addEventListener('click', () => { | ||
|  |         Zone.current.scheduleMicroTask('test', () => log += 'microtask;'); | ||
|  |         log += 'click;'; | ||
|  |       }); | ||
|  |       button.click(); | ||
|  | 
 | ||
|  |       expect(log).toEqual('click;microtask;'); | ||
|  |       done(); | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should work with addEventListener when called with an EventListener-implementing listener', | ||
|  |      function() { | ||
|  |        const eventListener = { | ||
|  |          x: 5, | ||
|  |          handleEvent: function(event: Event) { | ||
|  |            // Test that context is preserved
 | ||
|  |            expect(this.x).toBe(5); | ||
|  | 
 | ||
|  |            expect(event).toBe(clickEvent); | ||
|  |          } | ||
|  |        }; | ||
|  | 
 | ||
|  |        const clickEvent = document.createEvent('Event'); | ||
|  |        clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |        button.addEventListener('click', eventListener); | ||
|  | 
 | ||
|  |        button.dispatchEvent(clickEvent); | ||
|  |      }); | ||
|  | 
 | ||
|  |   it('should respect removeEventListener when called with a function listener', function() { | ||
|  |     let log = ''; | ||
|  |     const logFunction = function logFunction() { log += 'a'; }; | ||
|  | 
 | ||
|  |     button.addEventListener('click', logFunction); | ||
|  |     button.addEventListener('focus', logFunction); | ||
|  |     button.click(); | ||
|  |     expect(log).toEqual('a'); | ||
|  |     const focusEvent = document.createEvent('Event'); | ||
|  |     focusEvent.initEvent('focus', true, true); | ||
|  |     button.dispatchEvent(focusEvent); | ||
|  |     expect(log).toEqual('aa'); | ||
|  | 
 | ||
|  |     button.removeEventListener('click', logFunction); | ||
|  |     button.click(); | ||
|  |     expect(log).toEqual('aa'); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should respect removeEventListener with an EventListener-implementing listener', function() { | ||
|  |     const eventListener = {x: 5, handleEvent: jasmine.createSpy('handleEvent')}; | ||
|  | 
 | ||
|  |     button.addEventListener('click', eventListener); | ||
|  |     button.removeEventListener('click', eventListener); | ||
|  | 
 | ||
|  |     button.click(); | ||
|  | 
 | ||
|  |     expect(eventListener.handleEvent).not.toHaveBeenCalled(); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should have no effect while calling addEventListener without listener', function() { | ||
|  |     const onAddEventListenerSpy = jasmine.createSpy('addEventListener'); | ||
|  |     const eventListenerZone = | ||
|  |         Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy}); | ||
|  |     expect(function() { | ||
|  |       eventListenerZone.run(function() { | ||
|  |         button.addEventListener('click', null as any); | ||
|  |         button.addEventListener('click', undefined as any); | ||
|  |       }); | ||
|  |     }).not.toThrowError(); | ||
|  |     expect(onAddEventListenerSpy).not.toHaveBeenCalledWith(); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should have no effect while calling removeEventListener without listener', function() { | ||
|  |     const onAddEventListenerSpy = jasmine.createSpy('removeEventListener'); | ||
|  |     const eventListenerZone = | ||
|  |         Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy}); | ||
|  |     expect(function() { | ||
|  |       eventListenerZone.run(function() { | ||
|  |         button.removeEventListener('click', null as any); | ||
|  |         button.removeEventListener('click', undefined as any); | ||
|  |       }); | ||
|  |     }).not.toThrowError(); | ||
|  |     expect(onAddEventListenerSpy).not.toHaveBeenCalledWith(); | ||
|  |   }); | ||
|  | 
 | ||
|  | 
 | ||
|  |   it('should only add a listener once for a given set of arguments', function() { | ||
|  |     const log: string[] = []; | ||
|  |     const clickEvent = document.createEvent('Event'); | ||
|  | 
 | ||
|  |     function listener() { log.push('listener'); } | ||
|  | 
 | ||
|  |     clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |     button.addEventListener('click', listener); | ||
|  |     button.addEventListener('click', listener); | ||
|  |     button.addEventListener('click', listener); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  |     expect(log).toEqual(['listener']); | ||
|  | 
 | ||
|  |     button.removeEventListener('click', listener); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  |     expect(log).toEqual(['listener']); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should correctly handler capturing versus nonCapturing eventListeners', function() { | ||
|  |     const log: string[] = []; | ||
|  |     const clickEvent = document.createEvent('Event'); | ||
|  | 
 | ||
|  |     function capturingListener() { log.push('capturingListener'); } | ||
|  | 
 | ||
|  |     function bubblingListener() { log.push('bubblingListener'); } | ||
|  | 
 | ||
|  |     clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |     document.body.addEventListener('click', capturingListener, true); | ||
|  |     document.body.addEventListener('click', bubblingListener); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  | 
 | ||
|  |     expect(log).toEqual(['capturingListener', 'bubblingListener']); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should correctly handler a listener that is both capturing and nonCapturing', function() { | ||
|  |     const log: string[] = []; | ||
|  |     const clickEvent = document.createEvent('Event'); | ||
|  | 
 | ||
|  |     function listener() { log.push('listener'); } | ||
|  | 
 | ||
|  |     clickEvent.initEvent('click', true, true); | ||
|  | 
 | ||
|  |     document.body.addEventListener('click', listener, true); | ||
|  |     document.body.addEventListener('click', listener); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  | 
 | ||
|  |     document.body.removeEventListener('click', listener, true); | ||
|  |     document.body.removeEventListener('click', listener); | ||
|  | 
 | ||
|  |     button.dispatchEvent(clickEvent); | ||
|  | 
 | ||
|  |     expect(log).toEqual(['listener', 'listener']); | ||
|  |   }); | ||
|  | 
 | ||
|  |   describe('onclick', function() { | ||
|  |     function supportsOnClick() { | ||
|  |       const div = document.createElement('div'); | ||
|  |       const clickPropDesc = Object.getOwnPropertyDescriptor(div, 'onclick'); | ||
|  |       return !( | ||
|  |           EventTarget && div instanceof EventTarget && clickPropDesc && | ||
|  |           clickPropDesc.value === null); | ||
|  |     } | ||
|  |     (<any>supportsOnClick).message = 'Supports Element#onclick patching'; | ||
|  | 
 | ||
|  | 
 | ||
|  |     ifEnvSupports(supportsOnClick, function() { | ||
|  |       it('should spawn new child zones', function() { | ||
|  |         let run = false; | ||
|  |         button.onclick = function() { run = true; }; | ||
|  | 
 | ||
|  |         button.click(); | ||
|  |         expect(run).toBeTruthy(); | ||
|  |       }); | ||
|  |     }); | ||
|  | 
 | ||
|  | 
 | ||
|  |     it('should only allow one onclick handler', function() { | ||
|  |       let log = ''; | ||
|  |       button.onclick = function() { log += 'a'; }; | ||
|  |       button.onclick = function() { log += 'b'; }; | ||
|  | 
 | ||
|  |       button.click(); | ||
|  |       expect(log).toEqual('b'); | ||
|  |     }); | ||
|  | 
 | ||
|  | 
 | ||
|  |     it('should handler removing onclick', function() { | ||
|  |       let log = ''; | ||
|  |       button.onclick = function() { log += 'a'; }; | ||
|  |       button.onclick = null as any; | ||
|  | 
 | ||
|  |       button.click(); | ||
|  |       expect(log).toEqual(''); | ||
|  |     }); | ||
|  | 
 | ||
|  |     it('should be able to deregister the same event twice', function() { | ||
|  |       const listener = (event: Event) => {}; | ||
|  |       document.body.addEventListener('click', listener, false); | ||
|  |       document.body.removeEventListener('click', listener, false); | ||
|  |       document.body.removeEventListener('click', listener, false); | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   describe('onEvent default behavior', function() { | ||
|  |     let checkbox: HTMLInputElement; | ||
|  |     beforeEach(function() { | ||
|  |       checkbox = document.createElement('input'); | ||
|  |       checkbox.type = 'checkbox'; | ||
|  |       document.body.appendChild(checkbox); | ||
|  |     }); | ||
|  | 
 | ||
|  |     afterEach(function() { document.body.removeChild(checkbox); }); | ||
|  | 
 | ||
|  |     it('should be possible to prevent default behavior by returning false', function() { | ||
|  |       checkbox.onclick = function() { return false; }; | ||
|  | 
 | ||
|  |       checkbox.click(); | ||
|  |       expect(checkbox.checked).toBe(false); | ||
|  |     }); | ||
|  | 
 | ||
|  |     it('should have no effect on default behavior when not returning anything', function() { | ||
|  |       checkbox.onclick = function() {}; | ||
|  | 
 | ||
|  |       checkbox.click(); | ||
|  |       expect(checkbox.checked).toBe(true); | ||
|  |     }); | ||
|  |   }); | ||
|  | }); |