ipsos-components
Version:
Material Design components for Angular
635 lines (459 loc) • 24.4 kB
text/typescript
import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {Platform} from '@angular/cdk/platform';
import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing';
import {RIPPLE_FADE_OUT_DURATION, RIPPLE_FADE_IN_DURATION} from './ripple-renderer';
import {
MatRipple, MatRippleModule, MAT_RIPPLE_GLOBAL_OPTIONS, RippleState, RippleGlobalOptions
} from './index';
describe('MatRipple', () => {
let fixture: ComponentFixture<any>;
let rippleTarget: HTMLElement;
let originalBodyMargin: string | null;
let platform: Platform;
/** Extracts the numeric value of a pixel size string like '123px'. */
const pxStringToFloat = s => parseFloat(s) || 0;
const startingWindowWidth = window.innerWidth;
const startingWindowHeight = window.innerHeight;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatRippleModule],
declarations: [
BasicRippleContainer,
RippleContainerWithInputBindings,
RippleContainerWithoutBindings,
RippleContainerWithNgIf,
],
});
});
beforeEach(inject([Platform], (p: Platform) => {
platform = p;
// Set body margin to 0 during tests so it doesn't mess up position calculations.
originalBodyMargin = document.body.style.margin;
document.body.style.margin = '0';
}));
afterEach(() => {
document.body.style.margin = originalBodyMargin;
});
describe('basic ripple', () => {
let rippleDirective: MatRipple;
const TARGET_HEIGHT = 200;
const TARGET_WIDTH = 300;
beforeEach(() => {
fixture = TestBed.createComponent(BasicRippleContainer);
fixture.detectChanges();
rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]');
rippleDirective = fixture.componentInstance.ripple;
});
it('sizes ripple to cover element', () => {
// This test is consistently flaky on iOS (vs. Safari on desktop and all other browsers).
// Temporarily skip this test on iOS until we can determine the source of the flakiness.
// TODO(jelbourn): determine the source of flakiness here
if (platform.IOS) {
return;
}
let elementRect = rippleTarget.getBoundingClientRect();
// Dispatch a ripple at the following relative coordinates (X: 50| Y: 75)
dispatchMouseEvent(rippleTarget, 'mousedown', 50, 75);
dispatchMouseEvent(rippleTarget, 'mouseup');
// Calculate distance from the click to farthest edge of the ripple target.
let maxDistanceX = TARGET_WIDTH - 50;
let maxDistanceY = TARGET_HEIGHT - 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.
let expectedRadius = Math.sqrt(maxDistanceX * maxDistanceX + maxDistanceY * maxDistanceY);
let expectedLeft = elementRect.left + 50 - expectedRadius;
let expectedTop = elementRect.top + 75 - expectedRadius;
let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement;
// Note: getBoundingClientRect won't work because there's a transform applied to make the
// ripple start out tiny.
expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1);
expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1);
expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1);
expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * expectedRadius, 1);
});
it('creates ripple on mousedown', () => {
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(2);
});
it('should launch ripples on touchstart', fakeAsync(() => {
dispatchTouchEvent(rippleTarget, 'touchstart');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(RIPPLE_FADE_IN_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
dispatchTouchEvent(rippleTarget, 'touchend');
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should ignore synthetic mouse events after touchstart', () => fakeAsync(() => {
dispatchTouchEvent(rippleTarget, 'touchstart');
dispatchTouchEvent(rippleTarget, 'mousedown');
tick(RIPPLE_FADE_IN_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
dispatchTouchEvent(rippleTarget, 'touchend');
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('removes ripple after timeout', fakeAsync(() => {
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
// Calculates the duration for fading-in and fading-out the ripple.
tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should remove ripples after mouseup', fakeAsync(() => {
dispatchMouseEvent(rippleTarget, 'mousedown');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
// Fakes the duration of fading-in and fading-out normal ripples.
// The fade-out duration has been added to ensure that didn't start fading out.
tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
dispatchMouseEvent(rippleTarget, 'mouseup');
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should not hide ripples while animating.', fakeAsync(() => {
// Calculates the duration for fading-in and fading-out the ripple.
let hideDuration = RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION;
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(hideDuration - 10);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(10);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('creates ripples when manually triggered', () => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
rippleDirective.launch(0, 0);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
});
it('creates manual ripples with the default ripple config', () => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
// Calculate the diagonal distance and divide it by two for the center radius.
let radius = Math.sqrt(TARGET_HEIGHT * TARGET_HEIGHT + TARGET_WIDTH * TARGET_WIDTH) / 2;
rippleDirective.centered = true;
rippleDirective.launch(0, 0);
let rippleElement = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement;
expect(rippleElement).toBeTruthy();
expect(parseFloat(rippleElement.style.left as string))
.toBeCloseTo(TARGET_WIDTH / 2 - radius, 1);
expect(parseFloat(rippleElement.style.top as string))
.toBeCloseTo(TARGET_HEIGHT / 2 - radius, 1);
});
it('cleans up the event handlers when the container gets destroyed', () => {
fixture = TestBed.createComponent(RippleContainerWithNgIf);
fixture.detectChanges();
rippleTarget = fixture.debugElement.nativeElement.querySelector('[mat-ripple]');
fixture.componentInstance.isDestroyed = true;
fixture.detectChanges();
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
});
it('does not run events inside the NgZone', () => {
const spy = jasmine.createSpy('zone unstable callback');
const subscription = fixture.ngZone!.onUnstable.subscribe(spy);
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});
describe('when page is scrolled', () => {
let veryLargeElement: HTMLDivElement = document.createElement('div');
let pageScrollTop = 500;
let pageScrollLeft = 500;
beforeEach(() => {
// Add a very large element to make the page scroll
veryLargeElement.style.width = '4000px';
veryLargeElement.style.height = '4000px';
document.body.appendChild(veryLargeElement);
document.body.scrollTop = pageScrollTop;
document.body.scrollLeft = pageScrollLeft;
// Firefox
document.documentElement.scrollLeft = pageScrollLeft;
document.documentElement.scrollTop = pageScrollTop;
// Mobile safari
window.scrollTo(pageScrollLeft, pageScrollTop);
});
afterEach(() => {
document.body.removeChild(veryLargeElement);
document.body.scrollTop = 0;
document.body.scrollLeft = 0;
// Firefox
document.documentElement.scrollLeft = 0;
document.documentElement.scrollTop = 0;
// Mobile safari
window.scrollTo(0, 0);
});
it('create ripple with correct position', () => {
let elementTop = 600;
let elementLeft = 750;
let left = 50;
let top = 75;
rippleTarget.style.left = `${elementLeft}px`;
rippleTarget.style.top = `${elementTop}px`;
// Simulate a keyboard-triggered click by setting event coordinates to 0.
dispatchMouseEvent(rippleTarget, 'mousedown',
left + elementLeft - pageScrollLeft,
top + elementTop - pageScrollTop
);
let expectedRadius = Math.sqrt(250 * 250 + 125 * 125);
let expectedLeft = left - expectedRadius;
let expectedTop = top - expectedRadius;
let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement;
// In the iOS simulator (BrowserStack & SauceLabs), adding the content to the
// body causes karma's iframe for the test to stretch to fit that content once we attempt to
// scroll the page. Setting width / height / maxWidth / maxHeight on the iframe does not
// successfully constrain its size. As such, skip assertions in environments where the
// window size has changed since the start of the test.
if (window.innerWidth > startingWindowWidth || window.innerHeight > startingWindowHeight) {
return;
}
expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1);
expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1);
expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1);
expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * expectedRadius, 1);
});
});
});
describe('manual ripples', () => {
let rippleDirective: MatRipple;
beforeEach(() => {
fixture = TestBed.createComponent(BasicRippleContainer);
fixture.detectChanges();
rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]');
rippleDirective = fixture.componentInstance.ripple;
});
it('should allow persistent ripple elements', fakeAsync(() => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
let rippleRef = rippleDirective.launch(0, 0, { persistent: true });
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
// Calculates the duration for fading-in and fading-out the ripple. Also adds some
// extra time to demonstrate that the ripples are persistent.
tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION + 5000);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
rippleRef.fadeOut();
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should remove ripples that are not done fading-in', fakeAsync(() => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
rippleDirective.launch(0, 0);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(RIPPLE_FADE_IN_DURATION / 2);
rippleDirective.fadeOutAll();
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripples to be active after calling fadeOutAll.');
}));
it('should properly set ripple states', fakeAsync(() => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
let rippleRef = rippleDirective.launch(0, 0, { persistent: true });
expect(rippleRef.state).toBe(RippleState.FADING_IN);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(RIPPLE_FADE_IN_DURATION);
expect(rippleRef.state).toBe(RippleState.VISIBLE);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
rippleRef.fadeOut();
expect(rippleRef.state).toBe(RippleState.FADING_OUT);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
tick(RIPPLE_FADE_OUT_DURATION);
expect(rippleRef.state).toBe(RippleState.HIDDEN);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
});
describe('global ripple options', () => {
let rippleDirective: MatRipple;
function createTestComponent(rippleConfig: RippleGlobalOptions,
testComponent: any = BasicRippleContainer) {
// Reset the previously configured testing module to be able set new providers.
// The testing module has been initialized in the root describe group for the ripples.
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [MatRippleModule],
declarations: [testComponent],
providers: [{ provide: MAT_RIPPLE_GLOBAL_OPTIONS, useValue: rippleConfig }]
});
fixture = TestBed.createComponent(testComponent);
fixture.detectChanges();
rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]');
rippleDirective = fixture.componentInstance.ripple;
}
it('should work without having any binding set', () => {
createTestComponent({ disabled: true }, RippleContainerWithoutBindings);
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
});
it('when disabled should not show any ripples on mousedown', () => {
createTestComponent({ disabled: true });
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
});
it('when disabled should still allow manual ripples', () => {
createTestComponent({ disabled: true });
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
rippleDirective.launch(0, 0);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
});
it('should support changing the baseSpeedFactor', fakeAsync(() => {
createTestComponent({ baseSpeedFactor: 0.5 });
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
// Calculates the speedFactor for the duration. Those factors needs to be inverted, because
// a lower speed factor, will make the duration longer. For example: 0.5 => 2x duration.
let fadeInFactor = 1 / 0.5;
// Calculates the duration for fading-in and fading-out the ripple.
tick(RIPPLE_FADE_IN_DURATION * fadeInFactor + RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should combine individual speed factor with baseSpeedFactor', fakeAsync(() => {
createTestComponent({ baseSpeedFactor: 0.5 });
rippleDirective.launch(0, 0, { speedFactor: 1.5 });
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
// Calculates the speedFactor for the duration. Those factors needs to be inverted, because
// a lower speed factor, will make the duration longer. For example: 0.5 => 2x duration.
let fadeInFactor = 1 / (0.5 * 1.5);
// Calculates the duration for fading-in and fading-out the ripple.
tick(RIPPLE_FADE_IN_DURATION * fadeInFactor + RIPPLE_FADE_OUT_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
});
describe('configuring behavior', () => {
let controller: RippleContainerWithInputBindings;
let rippleComponent: MatRipple;
beforeEach(() => {
fixture = TestBed.createComponent(RippleContainerWithInputBindings);
fixture.detectChanges();
controller = fixture.debugElement.componentInstance;
rippleComponent = controller.ripple;
rippleTarget = fixture.debugElement.nativeElement.querySelector('[mat-ripple]');
});
it('sets ripple color', () => {
let backgroundColor = 'rgba(12, 34, 56, 0.8)';
controller.color = backgroundColor;
fixture.detectChanges();
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
let ripple = rippleTarget.querySelector('.mat-ripple-element')!;
expect(window.getComputedStyle(ripple).backgroundColor).toBe(backgroundColor);
});
it('does not respond to events when disabled input is set', () => {
controller.disabled = true;
fixture.detectChanges();
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
controller.disabled = false;
fixture.detectChanges();
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
});
it('allows specifying custom trigger element', () => {
let alternateTrigger = fixture.debugElement.nativeElement
.querySelector('.alternateTrigger') as HTMLElement;
dispatchMouseEvent(alternateTrigger, 'mousedown');
dispatchMouseEvent(alternateTrigger, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
// Set the trigger element, and now events should create ripples.
controller.trigger = alternateTrigger;
fixture.detectChanges();
dispatchMouseEvent(alternateTrigger, 'mousedown');
dispatchMouseEvent(alternateTrigger, 'mouseup');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
});
it('expands ripple from center if centered input is set', () => {
controller.centered = true;
fixture.detectChanges();
let elementRect = rippleTarget.getBoundingClientRect();
// Click the ripple element 50 px to the right and 75px down from its upper left.
dispatchMouseEvent(rippleTarget, 'mousedown', 50, 75);
dispatchMouseEvent(rippleTarget, 'mouseup');
// 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.
let expectedRadius = Math.sqrt(150 * 150 + 100 * 100);
let expectedLeft = elementRect.left + (elementRect.width / 2) - expectedRadius;
let expectedTop = elementRect.top + (elementRect.height / 2) - expectedRadius;
let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement;
expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1);
expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1);
expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1);
expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * expectedRadius, 1);
});
it('uses custom radius if set', () => {
let customRadius = 42;
controller.radius = customRadius;
fixture.detectChanges();
let elementRect = rippleTarget.getBoundingClientRect();
// Click the ripple element 50 px to the right and 75px down from its upper left.
dispatchMouseEvent(rippleTarget, 'mousedown', 50, 75);
dispatchMouseEvent(rippleTarget, 'mouseup');
let expectedLeft = elementRect.left + 50 - customRadius;
let expectedTop = elementRect.top + 75 - customRadius;
let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement;
expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1);
expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1);
expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * customRadius, 1);
expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * customRadius, 1);
});
});
});
({
template: `
<div id="container" #ripple="matRipple" mat-ripple [matRippleSpeedFactor]="0"
style="position: relative; width:300px; height:200px;">
</div>
`,
})
class BasicRippleContainer {
('ripple') ripple: MatRipple;
}
({
template: `
<div id="container" style="position: relative; width:300px; height:200px;"
mat-ripple
[matRippleSpeedFactor]="0"
[matRippleTrigger]="trigger"
[matRippleCentered]="centered"
[matRippleRadius]="radius"
[matRippleDisabled]="disabled"
[matRippleColor]="color">
</div>
<div class="alternateTrigger"></div>
`,
})
class RippleContainerWithInputBindings {
trigger: HTMLElement;
centered = false;
disabled = false;
radius = 0;
color = '';
(MatRipple) ripple: MatRipple;
}
({
template: `<div id="container" #ripple="matRipple" mat-ripple></div>`,
})
class RippleContainerWithoutBindings {}
({ template: `<div id="container" mat-ripple [matRippleSpeedFactor]="0"
*ngIf="!isDestroyed"></div>` })
class RippleContainerWithNgIf {
(MatRipple) ripple: MatRipple;
isDestroyed = false;
}