2019-05-31 11:56:07 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2019-05-31 11:56:07 -04:00
|
|
|
*
|
|
|
|
* 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);
|
|
|
|
});
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
afterEach(function() {
|
|
|
|
document.body.removeChild(button);
|
|
|
|
});
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
expect(event).toBe(clickEvent as any);
|
|
|
|
});
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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 = '';
|
2020-04-13 19:40:21 -04:00
|
|
|
const logFunction = function logFunction() {
|
|
|
|
log += 'a';
|
|
|
|
};
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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');
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
function listener() {
|
|
|
|
log.push('listener');
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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');
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
function capturingListener() {
|
|
|
|
log.push('capturingListener');
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
function bubblingListener() {
|
|
|
|
log.push('bubblingListener');
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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');
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
function listener() {
|
|
|
|
log.push('listener');
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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;
|
2020-04-13 19:40:21 -04:00
|
|
|
button.onclick = function() {
|
|
|
|
run = true;
|
|
|
|
};
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
button.click();
|
|
|
|
expect(run).toBeTruthy();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should only allow one onclick handler', function() {
|
|
|
|
let log = '';
|
2020-04-13 19:40:21 -04:00
|
|
|
button.onclick = function() {
|
|
|
|
log += 'a';
|
|
|
|
};
|
|
|
|
button.onclick = function() {
|
|
|
|
log += 'b';
|
|
|
|
};
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
button.click();
|
|
|
|
expect(log).toEqual('b');
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should handler removing onclick', function() {
|
|
|
|
let log = '';
|
2020-04-13 19:40:21 -04:00
|
|
|
button.onclick = function() {
|
|
|
|
log += 'a';
|
|
|
|
};
|
2019-05-31 11:56:07 -04:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
afterEach(function() {
|
|
|
|
document.body.removeChild(checkbox);
|
|
|
|
});
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
it('should be possible to prevent default behavior by returning false', function() {
|
2020-04-13 19:40:21 -04:00
|
|
|
checkbox.onclick = function() {
|
|
|
|
return false;
|
|
|
|
};
|
2019-05-31 11:56:07 -04:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|