/** * @license * Copyright Google LLC 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); } (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); }); }); });