diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 57a5e4ca4fb0..b9ad15019cd7 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -13,6 +13,7 @@ import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {MdAutocomplete} from './autocomplete'; import {MdInputContainer} from '../input/input-container'; +import {dispatchFakeEvent} from '../core/testing/dispatch-events'; describe('MdAutocomplete', () => { let overlayContainerElement: HTMLElement; @@ -61,7 +62,7 @@ describe('MdAutocomplete', () => { expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to start out closed.`); - dispatchEvent('focus', input); + dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) @@ -88,11 +89,11 @@ describe('MdAutocomplete', () => { }); it('should close the panel when blurred', async(() => { - dispatchEvent('focus', input); + dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); fixture.whenStable().then(() => { - dispatchEvent('blur', input); + dispatchFakeEvent(input, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) @@ -103,7 +104,7 @@ describe('MdAutocomplete', () => { })); it('should close the panel when an option is clicked', async(() => { - dispatchEvent('focus', input); + dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -119,13 +120,13 @@ describe('MdAutocomplete', () => { })); it('should close the panel when a newly created option is clicked', async(() => { - dispatchEvent('focus', input); + dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); fixture.whenStable().then(() => { // Filter down the option list to a subset of original options ('Alabama', 'California') input.value = 'al'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); let options = @@ -135,7 +136,7 @@ describe('MdAutocomplete', () => { // Changing value from 'Alabama' to 'al' to re-populate the option list, // ensuring that 'California' is created new. input.value = 'al'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -166,13 +167,13 @@ describe('MdAutocomplete', () => { }); it('should close the panel when the options list is empty', async(() => { - dispatchEvent('focus', input); + dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); fixture.whenStable().then(() => { // Filter down the option list such that no options match the value input.value = 'af'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) @@ -232,14 +233,14 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); input.value = 'a'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .toEqual('a', 'Expected control value to be updated as user types.'); input.value = 'al'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) @@ -273,7 +274,7 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); input.value = 'Californi'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) @@ -330,7 +331,7 @@ describe('MdAutocomplete', () => { it('should clear the text field if value is reset programmatically', async(() => { input.value = 'Alabama'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -367,7 +368,7 @@ describe('MdAutocomplete', () => { .toBe(false, `Expected control to start out pristine.`); input.value = 'a'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) @@ -409,7 +410,7 @@ describe('MdAutocomplete', () => { expect(fixture.componentInstance.stateCtrl.touched) .toBe(false, `Expected control to start out untouched.`); - dispatchEvent('blur', input); + dispatchFakeEvent(input, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) @@ -429,8 +430,8 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent; - ENTER_EVENT = new FakeKeyboardEvent(ENTER) as KeyboardEvent; + DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent; + ENTER_EVENT = new MockKeyboardEvent(ENTER) as KeyboardEvent; fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); @@ -491,7 +492,7 @@ describe('MdAutocomplete', () => { const optionEls = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - const UP_ARROW_EVENT = new FakeKeyboardEvent(UP_ARROW) as KeyboardEvent; + const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent; fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.whenStable().then(() => { @@ -522,7 +523,7 @@ describe('MdAutocomplete', () => { fixture.whenStable().then(() => { input.value = 'o'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); @@ -556,10 +557,10 @@ describe('MdAutocomplete', () => { it('should fill the text field, not select an option, when SPACE is entered', async(() => { fixture.whenStable().then(() => { input.value = 'New'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); - const SPACE_EVENT = new FakeKeyboardEvent(SPACE) as KeyboardEvent; + const SPACE_EVENT = new MockKeyboardEvent(SPACE) as KeyboardEvent; fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT); fixture.detectChanges(); @@ -595,7 +596,7 @@ describe('MdAutocomplete', () => { .toEqual('', `Expected panel to close after ENTER key.`); input.value = 'Alabam'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) @@ -669,7 +670,7 @@ describe('MdAutocomplete', () => { expect(input.hasAttribute('aria-activedescendant')) .toBe(false, 'Expected aria-activedescendant to be absent if no active item.'); - const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent; + const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent; fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); @@ -773,7 +774,7 @@ describe('MdAutocomplete', () => { fixture.whenStable().then(() => { input.value = 'f'; - dispatchEvent('input', input); + dispatchFakeEvent(input, 'input'); fixture.detectChanges(); const inputTop = input.getBoundingClientRect().top; @@ -848,24 +849,8 @@ class SimpleAutocomplete implements OnDestroy { } - - -/** - * TODO: Move this to core testing utility until Angular has event faking - * support. - * - * Dispatches an event from an element. - * @param eventName Name of the event - * @param element The element from which the event will be dispatched. - */ -function dispatchEvent(eventName: string, element: HTMLElement): void { - let event = document.createEvent('Event'); - event.initEvent(eventName, true, true); - element.dispatchEvent(event); -} - /** This is a mock keyboard event to test keyboard events in the autocomplete. */ -class FakeKeyboardEvent { +class MockKeyboardEvent { constructor(public keyCode: number) {} preventDefault() {} } diff --git a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts index f342d56cb10d..ab576282ddb0 100644 --- a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts +++ b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts @@ -3,6 +3,7 @@ import {NgModule, Component, ViewChild, ElementRef, QueryList, ViewChildren} fro import {ScrollDispatcher} from './scroll-dispatcher'; import {OverlayModule} from '../overlay-directives'; import {Scrollable} from './scrollable'; +import {dispatchFakeEvent} from '../../testing/dispatch-events'; describe('Scroll Dispatcher', () => { @@ -51,9 +52,7 @@ describe('Scroll Dispatcher', () => { // Emit a scroll event from the scrolling element in our component. // This event should be picked up by the scrollable directive and notify. // The notification should be picked up by the service. - const scrollEvent = document.createEvent('UIEvents'); - scrollEvent.initUIEvent('scroll', true, true, window, 0); - fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(scrollEvent); + dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll'); expect(hasDirectiveScrollNotified).toBe(true); expect(hasServiceScrollNotified).toBe(true); diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 449e6845e0f5..bcbe7acbb009 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -2,54 +2,7 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/ import {Component, ViewChild} from '@angular/core'; import {MdRipple, MdRippleModule} from './ripple'; import {ViewportRuler} from '../overlay/position/viewport-ruler'; - - -/** Creates a DOM event to indicate that a CSS transition for the given property ended. */ -const createTransitionEndEvent = (propertyName: string) => { - // The "new" TransitionEvent constructor isn't available in anything except Firefox: - // https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent - // So we just try to create a base event, and IE11 doesn't support that so we have to use - // the deprecated initTransitionEvent. - try { - const event = new Event('transitionend'); - (event).propertyName = propertyName; - return event; - } catch (e) { - const event = document.createEvent('TransitionEvent'); - event.initTransitionEvent('transitionend', - false, /* canBubble */ - false, /* cancelable */ - propertyName, - 0 /* elapsedTime */); - return event; - } -}; - -/** Creates a DOM mouse event. */ -const createMouseEvent = (eventType: string, dict: any = {}) => { - // Ideally this would just be "return new MouseEvent(eventType, dict)". But IE11 doesn't support - // the MouseEvent constructor, and Edge inexplicably divides clientX and clientY by 100 to get - // pageX and pageY. (Really. After "e = new MouseEvent('click', {clientX: 200, clientY: 300})", - // e.clientX is 200, e.pageX is 2, e.clientY is 300, and e.pageY is 3.) - // So instead we use the deprecated createEvent/initMouseEvent API, which works everywhere. - const event = document.createEvent('MouseEvents'); - event.initMouseEvent(eventType, - false, /* canBubble */ - false, /* cancelable */ - window, /* view */ - 0, /* detail */ - dict.screenX || 0, - dict.screenY || 0, - dict.clientX || 0, - dict.clientY || 0, - false, /* ctrlKey */ - false, /* altKey */ - false, /* shiftKey */ - false, /* metaKey */ - 0, /* button */ - null /* relatedTarget */); - return event; -}; +import {dispatchMouseEvent, dispatchTransitionEndEvent} from '../testing/dispatch-events'; /** Extracts the numeric value of a pixel size string like '123px'. */ const pxStringToFloat = (s: string) => { @@ -98,13 +51,11 @@ describe('MdRipple', () => { it('shows background when parent receives mousedown event', () => { expect(rippleBackground.classList).not.toContain('md-ripple-active'); - const mouseDown = createMouseEvent('mousedown'); // mousedown on the ripple element activates the background ripple. - rippleElement.dispatchEvent(mouseDown); + dispatchMouseEvent(rippleElement, 'mousedown'); expect(rippleBackground.classList).toContain('md-ripple-active'); // mouseleave on the container removes the background ripple. - const mouseLeave = createMouseEvent('mouseleave'); - rippleElement.dispatchEvent(mouseLeave); + dispatchMouseEvent(rippleElement, 'mouseleave'); expect(rippleBackground.classList).not.toContain('md-ripple-active'); }); @@ -118,21 +69,20 @@ describe('MdRipple', () => { expect(ripples[0].classList).toContain('md-ripple-fade-in'); expect(ripples[1].classList).toContain('md-ripple-fade-in'); // Signal the end of the first ripple's expansion. The second ripple should be unaffected. - const opacityTransitionEnd = createTransitionEndEvent('opacity'); - ripples[0].dispatchEvent(opacityTransitionEnd); + dispatchTransitionEndEvent(ripples[0], 'opacity'); expect(ripples[0].classList).not.toContain('md-ripple-fade-in'); expect(ripples[0].classList).toContain('md-ripple-fade-out'); expect(ripples[1].classList).toContain('md-ripple-fade-in'); expect(ripples[1].classList).not.toContain('md-ripple-fade-out'); // Signal the end of the first ripple's fade out. The ripple should be removed from the DOM. - ripples[0].dispatchEvent(opacityTransitionEnd); + dispatchTransitionEndEvent(ripples[0], 'opacity'); expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(1); expect(rippleElement.querySelectorAll('.md-ripple-foreground')[0]).toBe(ripples[1]); // Finish the second ripple. - ripples[1].dispatchEvent(opacityTransitionEnd); + dispatchTransitionEndEvent(ripples[1], 'opacity'); expect(ripples[1].classList).not.toContain('md-ripple-fade-in'); expect(ripples[1].classList).toContain('md-ripple-fade-out'); - ripples[1].dispatchEvent(opacityTransitionEnd); + dispatchTransitionEndEvent(ripples[1], 'opacity'); expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(0); }); @@ -160,9 +110,7 @@ describe('MdRipple', () => { it('sizes ripple to cover element', () => { // Click the ripple element 50 px to the right and 75px down from its upper left. const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent(rippleElement, 'click', elementRect.left + 50, elementRect.top + 75); // At this point the foreground ripple should be created with a div centered at the click // location, and large enough to reach the furthest corner, which is 250px to the right // and 125px down relative to the click position. @@ -181,9 +129,7 @@ describe('MdRipple', () => { it('expands ripple from center on click event triggered by keyboard', () => { const elementRect = rippleElement.getBoundingClientRect(); // Simulate a keyboard-triggered click by setting event coordinates to 0. - const clickEvent = createMouseEvent('click', - {clientX: 0, clientY: 0, screenX: 0, screenY: 0}); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent(rippleElement, 'click'); // The foreground ripple should be centered in the middle of the bounding rect, and large // enough to reach the corners, which are all 150px horizontally and 100px vertically away. const expectedRadius = Math.sqrt(150 * 150 + 100 * 100); @@ -209,7 +155,7 @@ describe('MdRipple', () => { fixture.componentInstance.isDestroyed = true; fixture.detectChanges(); - rippleElement.dispatchEvent(createMouseEvent('mousedown')); + dispatchMouseEvent(rippleElement, 'mousedown'); expect(rippleBackground.classList).not.toContain('md-ripple-active'); }); @@ -261,13 +207,10 @@ describe('MdRipple', () => { rippleElement.style.top = `${elementTop}px`; // Simulate a keyboard-triggered click by setting event coordinates to 0. - const clickEvent = createMouseEvent('click', { - clientX: left + elementLeft - pageScrollLeft, - clientY: top + elementTop - pageScrollTop, - screenX: left + elementLeft, - screenY: top + elementTop - }); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent(rippleElement, 'click', + left + elementLeft - pageScrollLeft, + top + elementTop - pageScrollTop + ); const expectedRadius = Math.sqrt(250 * 250 + 125 * 125); const expectedLeft = left - expectedRadius; @@ -330,10 +273,9 @@ describe('MdRipple', () => { it('does not respond to events when disabled input is set', () => { controller.disabled = true; fixture.detectChanges(); - const mouseDown = createMouseEvent('mousedown'); // The background ripple should not respond to mouseDown, and no foreground ripple should be // created on a click. - rippleElement.dispatchEvent(mouseDown); + dispatchMouseEvent(rippleElement, 'mousedown'); expect(rippleBackground.classList).not.toContain('md-ripple-active'); rippleElement.click(); expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(0); @@ -348,8 +290,7 @@ describe('MdRipple', () => { // Events on the other div don't do anything by default. const alternateTrigger = fixture.debugElement.nativeElement.querySelector('.alternateTrigger'); - const mouseDown = createMouseEvent('mousedown'); - alternateTrigger.dispatchEvent(mouseDown); + dispatchMouseEvent(alternateTrigger, 'mousedown'); expect(rippleBackground.classList).not.toContain('md-ripple-active'); alternateTrigger.click(); expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(0); @@ -357,7 +298,7 @@ describe('MdRipple', () => { // Reassign the trigger element, and now events should create ripples. controller.trigger = alternateTrigger; fixture.detectChanges(); - alternateTrigger.dispatchEvent(mouseDown); + dispatchMouseEvent(alternateTrigger, 'mousedown'); expect(rippleBackground.classList).toContain('md-ripple-active'); alternateTrigger.click(); expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(1); @@ -368,9 +309,7 @@ describe('MdRipple', () => { fixture.detectChanges(); // Click the ripple element 50 px to the right and 75px down from its upper left. const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent(rippleElement, 'click', elementRect.left + 50, elementRect.top + 75); // Because the centered input is true, the center of the ripple should be the midpoint of the // bounding rect. The ripple should expand to cover the rect corners, which are 150px // horizontally and 100px vertically from the midpoint. @@ -391,9 +330,7 @@ describe('MdRipple', () => { fixture.detectChanges(); // Click the ripple element 50 px to the right and 75px down from its upper left. const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent(rippleElement, 'click', elementRect.left + 50, elementRect.top + 75); const expectedLeft = elementRect.left + 50 - customRadius; const expectedTop = elementRect.top + 75 - customRadius; diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index e3e86b2004c3..85a748066a59 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -6,6 +6,7 @@ import {TAB} from '../keyboard/keycodes'; import {FocusOriginMonitor} from './focus-classes'; import {PlatformModule} from '../platform/index'; import {Platform} from '../platform/platform'; +import {dispatchKeyboardEvent, dispatchMouseEvent} from '../testing/dispatch-events'; // NOTE: Firefox only fires focus & blur events when it is the currently active window. @@ -62,7 +63,7 @@ describe('FocusOriginMonitor', () => { if (platform.FIREFOX) { return; } // Simulate focus via keyboard. - dispatchKeydownEvent(document, TAB); + dispatchKeyboardEvent(document, TAB); buttonElement.focus(); fixture.detectChanges(); @@ -82,7 +83,7 @@ describe('FocusOriginMonitor', () => { if (platform.FIREFOX) { return; } // Simulate focus via mouse. - dispatchMousedownEvent(document); + dispatchMouseEvent(document, 'mousedown'); buttonElement.focus(); fixture.detectChanges(); @@ -205,7 +206,7 @@ describe('cdkFocusClasses', () => { if (platform.FIREFOX) { return; } // Simulate focus via keyboard. - dispatchKeydownEvent(document, TAB); + dispatchKeyboardEvent(document, TAB); buttonElement.focus(); fixture.detectChanges(); @@ -225,7 +226,7 @@ describe('cdkFocusClasses', () => { if (platform.FIREFOX) { return; } // Simulate focus via mouse. - dispatchMousedownEvent(document); + dispatchMouseEvent(document, 'mousedown'); buttonElement.focus(); fixture.detectChanges(); @@ -267,27 +268,5 @@ class PlainButton { constructor(public renderer: Renderer) {} } - @Component({template: ``}) class ButtonWithFocusClasses {} - - -/** Dispatches a mousedown event on the specified element. */ -function dispatchMousedownEvent(element: Node) { - let event = document.createEvent('MouseEvent'); - event.initMouseEvent( - 'mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - element.dispatchEvent(event); -} - - -/** Dispatches a keydown event on the specified element. */ -function dispatchKeydownEvent(element: Node, keyCode: number) { - let event: any = document.createEvent('KeyboardEvent'); - (event.initKeyEvent || event.initKeyboardEvent).bind(event)( - 'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode); - Object.defineProperty(event, 'keyCode', { - get: function() { return keyCode; } - }); - element.dispatchEvent(event); -} diff --git a/src/lib/core/testing/dispatch-events.ts b/src/lib/core/testing/dispatch-events.ts new file mode 100644 index 000000000000..78c973870155 --- /dev/null +++ b/src/lib/core/testing/dispatch-events.ts @@ -0,0 +1,26 @@ +import { + createFakeEvent, + createKeyboardEvent, + createMouseEvent, + createTransitionEndEvent +} from './event-objects'; + +/** Shorthand to dispatch a fake event on a specified node. */ +export function dispatchFakeEvent(node: Node, eventName: string) { + node.dispatchEvent(createFakeEvent(eventName)); +} + +/** Shorthand to dispatch a keyboard event with a specified key code. */ +export function dispatchKeyboardEvent(node: Node, keyCode: number, type = 'keydown') { + node.dispatchEvent(createKeyboardEvent(type, keyCode)); +} + +/** Shorthand to dispatch a mouse event on the specified coordinates. */ +export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0) { + node.dispatchEvent(createMouseEvent(type, x, y)); +} + +/** Shorthand to dispatch a transition event with a specified property. */ +export function dispatchTransitionEndEvent(node: Node, propertyName: string, elapsedTime = 0) { + node.dispatchEvent(createTransitionEndEvent(propertyName, elapsedTime)); +} diff --git a/src/lib/core/testing/event-objects.ts b/src/lib/core/testing/event-objects.ts new file mode 100644 index 000000000000..f07782dd6245 --- /dev/null +++ b/src/lib/core/testing/event-objects.ts @@ -0,0 +1,62 @@ +/** Creates a browser MouseEvent with the specified options. */ +export function createMouseEvent(type: string, x = 0, y = 0) { + let event = document.createEvent('MouseEvent'); + + event.initMouseEvent(type, + false, /* canBubble */ + false, /* cancelable */ + window, /* view */ + 0, /* detail */ + x, /* screenX */ + y, /* screenY */ + x, /* clientX */ + y, /* clientY */ + false, /* ctrlKey */ + false, /* altKey */ + false, /* shiftKey */ + false, /* metaKey */ + 0, /* button */ + null /* relatedTarget */); + + return event; +} + +/** Dispatches a keydown event from an element. */ +export function createKeyboardEvent(eventType: string, keyCode: number) { + let event = document.createEvent('KeyboardEvent') as any; + // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. + let initEventFn = (event.initKeyEvent || event.initKeyboardEvent).bind(event); + + initEventFn(eventType, true, true, window, 0, 0, 0, 0, 0, keyCode); + + // Webkit Browsers don't set the keyCode when calling the init function. + // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 + Object.defineProperty(event, 'keyCode', { + get: function() { return keyCode; } + }); + + return event; +} + +/** Creates a transition event with the specified property name. */ +export function createTransitionEndEvent(propertyName: string, elapsedTime = 0) { + // Some browsers have the TransitionEvent class, but once the class is being instantiated + // the browser will throw an exception. Those browsers don't support the constructor yet. + // To ensure that those browsers also work, the TransitionEvent is created by using the + // deprecated `initTransitionEvent` function. + try { + // TypeScript does not have valid types for the TransitionEvent class, so use `any`. + return new (TransitionEvent as any)('transitionend', {propertyName, elapsedTime}); + } catch (e) { + let event = document.createEvent('TransitionEvent'); + event.initTransitionEvent('transitionend', false, false, propertyName, elapsedTime); + return event; + } +} + +/** Creates a fake event object with any desired event type. */ +export function createFakeEvent(eventName: string) { + let event = document.createEvent('Event'); + event.initEvent(eventName, true, true); + return event; +} diff --git a/src/lib/radio/radio.spec.ts b/src/lib/radio/radio.spec.ts index 48cfe1a6599c..e5714a043ddd 100644 --- a/src/lib/radio/radio.spec.ts +++ b/src/lib/radio/radio.spec.ts @@ -5,6 +5,7 @@ import {By} from '@angular/platform-browser'; import {MdRadioGroup, MdRadioButton, MdRadioChange, MdRadioModule} from './radio'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; +import {dispatchFakeEvent} from '../core/testing/dispatch-events'; describe('MdRadio', () => { @@ -183,12 +184,12 @@ describe('MdRadio', () => { expect(nativeRadioInput.classList).not.toContain('md-radio-focused'); - dispatchEvent('focus', nativeRadioInput); + dispatchFakeEvent(nativeRadioInput, 'focus'); fixture.detectChanges(); expect(radioNativeElements[0].classList).toContain('md-radio-focused'); - dispatchEvent('blur', nativeRadioInput); + dispatchFakeEvent(nativeRadioInput, 'blur'); fixture.detectChanges(); expect(radioNativeElements[0].classList).not.toContain('md-radio-focused'); @@ -202,7 +203,7 @@ describe('MdRadio', () => { expect(radioNativeElements[0].classList).toContain('md-radio-focused'); - dispatchEvent('blur', nativeRadioInput); + dispatchFakeEvent(nativeRadioInput, 'blur'); fixture.detectChanges(); expect(radioNativeElements[0].classList).not.toContain('md-radio-focused'); @@ -421,7 +422,7 @@ describe('MdRadio', () => { })); it('should update the ngModel value when selecting a radio button', () => { - dispatchEvent('change', innerRadios[1].nativeElement); + dispatchFakeEvent(innerRadios[1].nativeElement, 'change'); fixture.detectChanges(); expect(testComponent.modelValue).toBe('chocolate'); }); @@ -430,11 +431,11 @@ describe('MdRadio', () => { expect(testComponent.modelValue).toBeUndefined(); expect(testComponent.lastEvent).toBeUndefined(); - dispatchEvent('change', innerRadios[1].nativeElement); + dispatchFakeEvent(innerRadios[1].nativeElement, 'change'); fixture.detectChanges(); expect(testComponent.lastEvent.value).toBe('chocolate'); - dispatchEvent('change', innerRadios[0].nativeElement); + dispatchFakeEvent(innerRadios[0].nativeElement, 'change'); fixture.detectChanges(); expect(testComponent.lastEvent.value).toBe('vanilla'); }); @@ -651,16 +652,3 @@ class RadioGroupWithNgModel { class RadioGroupWithFormControl { formControl = new FormControl(); } - -// TODO(jelbourn): remove everything below when Angular supports faking events. - -/** - * Dispatches an event from an element. - * @param eventName Name of the event - * @param element The element from which the event will be dispatched. - */ -function dispatchEvent(eventName: string, element: HTMLElement): void { - let event = document.createEvent('Event'); - event.initEvent(eventName, true, true); - element.dispatchEvent(event); -} diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 5f57794bdd80..6cab2282958d 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -10,6 +10,7 @@ import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; +import {dispatchFakeEvent} from '../core/testing/dispatch-events'; describe('MdSelect', () => { let overlayContainerElement: HTMLElement; @@ -408,7 +409,7 @@ describe('MdSelect', () => { .toEqual(false, `Expected the control to start off as untouched.`); trigger.click(); - dispatchEvent('blur', trigger); + dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.control.touched) .toEqual(false, `Expected the control to stay untouched when menu opened.`); @@ -416,7 +417,7 @@ describe('MdSelect', () => { const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; backdrop.click(); - dispatchEvent('blur', trigger); + dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.control.touched) .toEqual(true, `Expected the control to be touched as soon as focus left the select.`); @@ -1433,21 +1434,6 @@ class CompWithCustomSelect { @ViewChild(CustomSelectAccessor) customAccessor: CustomSelectAccessor; } - -/** - * TODO: Move this to core testing utility until Angular has event faking - * support. - * - * Dispatches an event from an element. - * @param eventName Name of the event - * @param element The element from which the event will be dispatched. - */ -function dispatchEvent(eventName: string, element: HTMLElement): void { - let event = document.createEvent('Event'); - event.initEvent(eventName, true, true); - element.dispatchEvent(event); -} - class FakeViewportRuler { getViewportRect() { return { diff --git a/src/lib/slide-toggle/slide-toggle.spec.ts b/src/lib/slide-toggle/slide-toggle.spec.ts index d956cfda2cbb..f14b418f0dbc 100644 --- a/src/lib/slide-toggle/slide-toggle.spec.ts +++ b/src/lib/slide-toggle/slide-toggle.spec.ts @@ -4,6 +4,7 @@ import {Component} from '@angular/core'; import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './slide-toggle'; import {FormsModule, NgControl, ReactiveFormsModule, FormControl} from '@angular/forms'; import {TestGestureConfig} from '../slider/test-gesture-config'; +import {dispatchFakeEvent} from '../core/testing/dispatch-events'; describe('MdSlideToggle', () => { @@ -329,7 +330,7 @@ describe('MdSlideToggle', () => { it('should correctly set the slide-toggle to checked on focus', () => { expect(slideToggleElement.classList).not.toContain('md-slide-toggle-focused'); - dispatchFocusChangeEvent('focus', inputElement); + dispatchFakeEvent(inputElement, 'focus'); fixture.detectChanges(); expect(slideToggleElement.classList).toContain('md-slide-toggle-focused'); @@ -570,17 +571,6 @@ describe('MdSlideToggle', () => { }); }); -/** - * Dispatches a focus change event from an element. - * @param eventName Name of the event, either 'focus' or 'blur'. - * @param element The element from which the event will be dispatched. - */ -function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void { - let event = document.createEvent('Event'); - event.initEvent(eventName, true, true); - element.dispatchEvent(event); -} - @Component({ selector: 'slide-toggle-test-app', template: ` diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index 99672f76eea7..a2e985302687 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -15,6 +15,7 @@ import { HOME, LEFT_ARROW } from '../core/keyboard/keycodes'; +import {dispatchKeyboardEvent, dispatchMouseEvent} from '../core/testing/dispatch-events'; describe('MdSlider', () => { @@ -728,7 +729,7 @@ describe('MdSlider', () => { it('should update the model on keydown', () => { expect(testComponent.val).toBe(0); - dispatchKeydownEvent(sliderNativeElement, UP_ARROW); + dispatchKeyboardEvent(sliderNativeElement, UP_ARROW); fixture.detectChanges(); expect(testComponent.val).toBe(1); @@ -948,14 +949,14 @@ describe('MdSlider', () => { }); it('should increment slider by 1 on up arrow pressed', () => { - dispatchKeydownEvent(sliderNativeElement, UP_ARROW); + dispatchKeyboardEvent(sliderNativeElement, UP_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(1); }); it('should increment slider by 1 on right arrow pressed', () => { - dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, RIGHT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(1); @@ -964,7 +965,7 @@ describe('MdSlider', () => { it('should decrement slider by 1 on down arrow pressed', () => { sliderInstance.value = 100; - dispatchKeydownEvent(sliderNativeElement, DOWN_ARROW); + dispatchKeyboardEvent(sliderNativeElement, DOWN_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(99); @@ -973,14 +974,14 @@ describe('MdSlider', () => { it('should decrement slider by 1 on left arrow pressed', () => { sliderInstance.value = 100; - dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, LEFT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(99); }); it('should increment slider by 10 on page up pressed', () => { - dispatchKeydownEvent(sliderNativeElement, PAGE_UP); + dispatchKeyboardEvent(sliderNativeElement, PAGE_UP); fixture.detectChanges(); expect(sliderInstance.value).toBe(10); @@ -989,14 +990,14 @@ describe('MdSlider', () => { it('should decrement slider by 10 on page down pressed', () => { sliderInstance.value = 100; - dispatchKeydownEvent(sliderNativeElement, PAGE_DOWN); + dispatchKeyboardEvent(sliderNativeElement, PAGE_DOWN); fixture.detectChanges(); expect(sliderInstance.value).toBe(90); }); it('should set slider to max on end pressed', () => { - dispatchKeydownEvent(sliderNativeElement, END); + dispatchKeyboardEvent(sliderNativeElement, END); fixture.detectChanges(); expect(sliderInstance.value).toBe(100); @@ -1005,7 +1006,7 @@ describe('MdSlider', () => { it('should set slider to min on home pressed', () => { sliderInstance.value = 100; - dispatchKeydownEvent(sliderNativeElement, HOME); + dispatchKeyboardEvent(sliderNativeElement, HOME); fixture.detectChanges(); expect(sliderInstance.value).toBe(0); @@ -1066,7 +1067,7 @@ describe('MdSlider', () => { testComponent.invert = true; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, RIGHT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(1); @@ -1077,7 +1078,7 @@ describe('MdSlider', () => { sliderInstance.value = 100; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, LEFT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(99); @@ -1088,7 +1089,7 @@ describe('MdSlider', () => { sliderInstance.value = 100; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, RIGHT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(99); @@ -1098,7 +1099,7 @@ describe('MdSlider', () => { testComponent.dir = 'rtl'; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, LEFT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(1); @@ -1110,7 +1111,7 @@ describe('MdSlider', () => { sliderInstance.value = 100; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, RIGHT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(99); @@ -1121,7 +1122,7 @@ describe('MdSlider', () => { testComponent.invert = true; fixture.detectChanges(); - dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, LEFT_ARROW); fixture.detectChanges(); expect(sliderInstance.value).toBe(1); @@ -1334,11 +1335,7 @@ function dispatchClickEventSequence(sliderElement: HTMLElement, percentage: numb let y = dimensions.top + (dimensions.height * percentage); dispatchMouseenterEvent(sliderElement); - - let event = document.createEvent('MouseEvent'); - event.initMouseEvent( - 'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); - sliderElement.dispatchEvent(event); + dispatchMouseEvent(sliderElement, 'click', x, y); } /** @@ -1426,23 +1423,5 @@ function dispatchMouseenterEvent(element: HTMLElement): void { let y = dimensions.top; let x = dimensions.left; - let event = document.createEvent('MouseEvent'); - event.initMouseEvent( - 'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); - element.dispatchEvent(event); -} - -/** - * Dispatches a keydown event from an element. - * @param element The element from which the event will be dispatched. - * @param keyCode The key code of the key being pressed. - */ -function dispatchKeydownEvent(element: HTMLElement, keyCode: number): void { - let event: any = document.createEvent('KeyboardEvent'); - (event.initKeyEvent || event.initKeyboardEvent).bind(event)( - 'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode); - Object.defineProperty(event, 'keyCode', { - get: function() { return keyCode; } - }); - element.dispatchEvent(event); + dispatchMouseEvent(element, 'mouseenter', x, y); } diff --git a/src/lib/tabs/tab-header.spec.ts b/src/lib/tabs/tab-header.spec.ts index a1e1030f0ec7..1a42139a5b78 100644 --- a/src/lib/tabs/tab-header.spec.ts +++ b/src/lib/tabs/tab-header.spec.ts @@ -10,6 +10,7 @@ import {MdTabLabelWrapper} from './tab-label-wrapper'; import {RIGHT_ARROW, LEFT_ARROW, ENTER} from '../core/keyboard/keycodes'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; +import {dispatchKeyboardEvent} from '../core/testing/dispatch-events'; describe('MdTabHeader', () => { @@ -106,18 +107,18 @@ describe('MdTabHeader', () => { expect(appComponent.mdTabHeader.focusIndex).toBe(0); // Move focus right to 2 - dispatchKeydownEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, RIGHT_ARROW); + dispatchKeyboardEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, RIGHT_ARROW); fixture.detectChanges(); expect(appComponent.mdTabHeader.focusIndex).toBe(2); // Select the focused index 2 expect(appComponent.selectedIndex).toBe(0); - dispatchKeydownEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, ENTER); + dispatchKeyboardEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, ENTER); fixture.detectChanges(); expect(appComponent.selectedIndex).toBe(2); // Move focus right to 0 - dispatchKeydownEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, LEFT_ARROW); + dispatchKeyboardEvent(appComponent.mdTabHeader._tabListContainer.nativeElement, LEFT_ARROW); fixture.detectChanges(); expect(appComponent.mdTabHeader.focusIndex).toBe(0); }); @@ -193,18 +194,6 @@ describe('MdTabHeader', () => { }); - -/** Dispatches a keydown event from an element. */ -function dispatchKeydownEvent(element: HTMLElement, keyCode: number): void { - let event: any = document.createEvent('KeyboardEvent'); - (event.initKeyEvent || event.initKeyboardEvent).bind(event)( - 'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode); - Object.defineProperty(event, 'keyCode', { - get: function() { return keyCode; } - }); - element.dispatchEvent(event); -} - interface Tab { label: string; disabled?: boolean; diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts index be613aefb389..2eb187bb6ad7 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts @@ -4,6 +4,7 @@ import {Component} from '@angular/core'; import {By} from '@angular/platform-browser'; import {ViewportRuler} from '../../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../../core/overlay/position/fake-viewport-ruler'; +import {dispatchMouseEvent} from '../../core/testing/dispatch-events'; describe('MdTabNavBar', () => { @@ -51,15 +52,11 @@ describe('MdTabNavBar', () => { let link = fixture.debugElement.nativeElement.querySelector('[md-tab-link]'); let rippleBackground = link.querySelector('.md-ripple-background'); - let mouseEvent = document.createEvent('MouseEvents'); fixture.componentInstance.isDestroyed = true; fixture.detectChanges(); - mouseEvent.initMouseEvent('mousedown', false, false, window, 0, 0, 0, 0, 0, false, false, - false, false, 0, null); - - link.dispatchEvent(mouseEvent); + dispatchMouseEvent(link, 'mousedown'); expect(rippleBackground.classList).not.toContain('md-ripple-active'); }); diff --git a/src/lib/tsconfig-srcs.json b/src/lib/tsconfig-srcs.json index fea7a527507f..f90842dfd36f 100644 --- a/src/lib/tsconfig-srcs.json +++ b/src/lib/tsconfig-srcs.json @@ -19,6 +19,9 @@ ] }, "exclude": [ + /* Exclude testing utilities in releases. */ + "core/testing/", + "**/*.spec.*", "system-config-spec.ts" ], diff --git a/tools/gulp/tasks/components.ts b/tools/gulp/tasks/components.ts index bf00567ebef9..7c871ae701f3 100644 --- a/tools/gulp/tasks/components.ts +++ b/tools/gulp/tasks/components.ts @@ -25,7 +25,7 @@ const gulpIf = require('gulp-if'); // for unit tests (karma). /** Path to the tsconfig used for ESM output. */ -const tsconfigPath = path.relative(PROJECT_ROOT, path.join(COMPONENTS_DIR, 'tsconfig.json')); +const tsconfigPath = path.relative(PROJECT_ROOT, path.join(COMPONENTS_DIR, 'tsconfig-srcs.json')); /** [Watch task] Rebuilds (ESM output) whenever ts, scss, or html sources change. */