ipsos-components
Version:
Material Design components for Angular
452 lines (361 loc) • 17.8 kB
text/typescript
import {inject, async, fakeAsync, tick, TestBed} from '@angular/core/testing';
import {SafeResourceUrl, DomSanitizer} from '@angular/platform-browser';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {Component} from '@angular/core';
import {MatIconModule} from './index';
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
import {FAKE_SVGS} from './fake-svgs';
import {wrappedErrorMessage} from '@angular/cdk/testing';
/** Returns the CSS classes assigned to an element as a sorted array. */
function sortedClassNames(element: Element): string[] {
return element.className.split(' ').sort();
}
/**
* Verifies that an element contains a single <svg> child element, and returns that child.
*/
function verifyAndGetSingleSvgChild(element: SVGElement): SVGElement {
expect(element.id).toBeFalsy();
expect(element.childNodes.length).toBe(1);
const svgChild = element.childNodes[0] as SVGElement;
expect(svgChild.tagName.toLowerCase()).toBe('svg');
return svgChild;
}
/**
* Verifies that an element contains a single <path> child element whose "id" attribute has
* the specified value.
*/
function verifyPathChildElement(element: Element, attributeValue: string): void {
expect(element.childNodes.length).toBe(1);
const pathElement = element.childNodes[0] as SVGPathElement;
expect(pathElement.tagName.toLowerCase()).toBe('path');
// The testing data SVGs have the name attribute set for verification.
expect(pathElement.getAttribute('name')).toBe(attributeValue);
}
describe('MatIcon', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, MatIconModule],
declarations: [
IconWithColor,
IconWithLigature,
IconWithCustomFontCss,
IconFromSvgName,
IconWithAriaHiddenFalse,
IconWithBindingAndNgIf,
]
});
TestBed.compileComponents();
}));
let iconRegistry: MatIconRegistry;
let http: HttpTestingController;
let sanitizer: DomSanitizer;
beforeEach(inject([MatIconRegistry, HttpTestingController, DomSanitizer],
(mir: MatIconRegistry, h: HttpTestingController, ds: DomSanitizer) => {
iconRegistry = mir;
http = h;
sanitizer = ds;
}));
it('should apply class based on color attribute', () => {
let fixture = TestBed.createComponent(IconWithColor);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'home';
testComponent.iconColor = 'primary';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['mat-icon', 'mat-primary', 'material-icons']);
});
it('should mark mat-icon as aria-hidden by default', () => {
const fixture = TestBed.createComponent(IconWithLigature);
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
expect(iconElement.getAttribute('aria-hidden'))
.toBe('true', 'Expected the mat-icon element has aria-hidden="true" by default');
});
it('should not override a user-provided aria-hidden attribute', () => {
const fixture = TestBed.createComponent(IconWithAriaHiddenFalse);
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
expect(iconElement.getAttribute('aria-hidden'))
.toBe('false', 'Expected the mat-icon element has the user-provided aria-hidden value');
});
describe('Ligature icons', () => {
it('should add material-icons class by default', () => {
let fixture = TestBed.createComponent(IconWithLigature);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'home';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['mat-icon', 'material-icons']);
});
it('should use alternate icon font if set', () => {
iconRegistry.setDefaultFontSetClass('myfont');
let fixture = TestBed.createComponent(IconWithLigature);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'home';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['mat-icon', 'myfont']);
});
});
describe('Icons from URLs', () => {
it('should register icon URLs by name', fakeAsync(() => {
iconRegistry.addSvgIcon('fluffy', trust('cat.svg'));
iconRegistry.addSvgIcon('fido', trust('dog.svg'));
let fixture = TestBed.createComponent(IconFromSvgName);
let svgElement: SVGElement;
const testComponent = fixture.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'fido';
fixture.detectChanges();
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
verifyPathChildElement(svgElement, 'woof');
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'fluffy';
fixture.detectChanges();
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
verifyPathChildElement(svgElement, 'meow');
// Using an icon from a previously loaded URL should not cause another HTTP request.
testComponent.iconName = 'fido';
fixture.detectChanges();
http.expectNone('dog.svg');
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
verifyPathChildElement(svgElement, 'woof');
// Assert that a registered icon can be looked-up by url.
iconRegistry.getSvgIconFromUrl(trust('cat.svg')).subscribe(element => {
verifyPathChildElement(element, 'meow');
});
tick();
}));
it('should throw an error when using an untrusted icon url', () => {
iconRegistry.addSvgIcon('fluffy', 'farm-set-1.svg');
expect(() => {
let fixture = TestBed.createComponent(IconFromSvgName);
fixture.componentInstance.iconName = 'fluffy';
fixture.detectChanges();
}).toThrowError(/unsafe value used in a resource URL context/);
});
it('should throw an error when using an untrusted icon set url', () => {
iconRegistry.addSvgIconSetInNamespace('farm', 'farm-set-1.svg');
expect(() => {
let fixture = TestBed.createComponent(IconFromSvgName);
fixture.componentInstance.iconName = 'farm:pig';
fixture.detectChanges();
}).toThrowError(/unsafe value used in a resource URL context/);
});
it('should extract icon from SVG icon set', () => {
iconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-1.svg'));
const fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
let svgElement: any;
let svgChild: any;
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
expect(matIconElement.childNodes.length).toBe(1);
svgElement = verifyAndGetSingleSvgChild(matIconElement);
expect(svgElement.childNodes.length).toBe(1);
svgChild = svgElement.childNodes[0];
// The first <svg> child should be the <g id="pig"> element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('name')).toBe('pig');
verifyPathChildElement(svgChild, 'oink');
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(matIconElement);
svgChild = svgElement.childNodes[0];
// The first <svg> child should be the <g id="cow"> element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('name')).toBe('cow');
verifyPathChildElement(svgChild, 'moo');
});
it('should allow multiple icon sets in a namespace', () => {
iconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-1.svg'));
iconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-2.svg'));
const fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
let svgElement: any;
let svgChild: any;
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
http.expectOne('farm-set-2.svg').flush(FAKE_SVGS.farmSet2);
svgElement = verifyAndGetSingleSvgChild(matIconElement);
expect(svgElement.childNodes.length).toBe(1);
svgChild = svgElement.childNodes[0];
// The <svg> child should be the <g id="pig"> element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('name')).toBe('pig');
expect(svgChild.getAttribute('id')).toBe('');
expect(svgChild.childNodes.length).toBe(1);
verifyPathChildElement(svgChild, 'oink');
// Change the icon name to one that appears in both icon sets. The icon from the set that
// was registered last should be used (with id attribute of 'moo moo' instead of 'moo'),
// and no additional HTTP request should be made.
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(matIconElement);
svgChild = svgElement.childNodes[0];
// The first <svg> child should be the <g id="cow"> element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('name')).toBe('cow');
expect(svgChild.childNodes.length).toBe(1);
verifyPathChildElement(svgChild, 'moo moo');
});
it('should unwrap <symbol> nodes', () => {
iconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-3.svg'));
const fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'farm:duck';
fixture.detectChanges();
http.expectOne('farm-set-3.svg').flush(FAKE_SVGS.farmSet3);
const svgElement = verifyAndGetSingleSvgChild(matIconElement);
const firstChild = svgElement.childNodes[0];
expect(svgElement.querySelector('symbol')).toBeFalsy();
expect(svgElement.childNodes.length).toBe(1);
expect(firstChild.nodeName.toLowerCase()).toBe('path');
expect((firstChild as HTMLElement).getAttribute('name')).toBe('quack');
});
it('should not wrap <svg> elements in icon sets in another svg tag', () => {
iconRegistry.addSvgIconSet(trust('arrow-set.svg'));
const fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
let svgElement: any;
testComponent.iconName = 'left-arrow';
fixture.detectChanges();
http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
// arrow-set.svg stores its icons as nested <svg> elements, so they should be used
// directly and not wrapped in an outer <svg> tag like the <g> elements in other sets.
svgElement = verifyAndGetSingleSvgChild(matIconElement);
verifyPathChildElement(svgElement, 'left');
});
it('should return unmodified copies of icons from icon sets', () => {
iconRegistry.addSvgIconSet(trust('arrow-set.svg'));
const fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
let svgElement: any;
testComponent.iconName = 'left-arrow';
fixture.detectChanges();
http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
svgElement = verifyAndGetSingleSvgChild(matIconElement);
verifyPathChildElement(svgElement, 'left');
// Modify the SVG element by setting a viewBox attribute.
svgElement.setAttribute('viewBox', '0 0 100 100');
// Switch to a different icon.
testComponent.iconName = 'right-arrow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(matIconElement);
verifyPathChildElement(svgElement, 'right');
// Switch back to the first icon. The viewBox attribute should not be present.
testComponent.iconName = 'left-arrow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(matIconElement);
verifyPathChildElement(svgElement, 'left');
expect(svgElement.getAttribute('viewBox')).toBeFalsy();
});
it('should not throw when toggling an icon that has a binding in IE11', () => {
iconRegistry.addSvgIcon('fluffy', trust('cat.svg'));
const fixture = TestBed.createComponent(IconWithBindingAndNgIf);
fixture.detectChanges();
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
expect(() => {
fixture.componentInstance.showIcon = false;
fixture.detectChanges();
fixture.componentInstance.showIcon = true;
fixture.detectChanges();
}).not.toThrow();
});
it('should remove the SVG element from the DOM when the binding is cleared', () => {
iconRegistry.addSvgIconSet(trust('arrow-set.svg'));
let fixture = TestBed.createComponent(IconFromSvgName);
const testComponent = fixture.componentInstance;
const icon = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.iconName = 'left-arrow';
fixture.detectChanges();
http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
expect(icon.querySelector('svg')).toBeTruthy();
testComponent.iconName = undefined;
fixture.detectChanges();
expect(icon.querySelector('svg')).toBeFalsy();
});
});
describe('custom fonts', () => {
it('should apply CSS classes for custom font and icon', () => {
iconRegistry.registerFontClassAlias('f1', 'font1');
iconRegistry.registerFontClassAlias('f2');
let fixture = TestBed.createComponent(IconWithCustomFontCss);
const testComponent = fixture.componentInstance;
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
testComponent.fontSet = 'f1';
testComponent.fontIcon = 'house';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['font1', 'house', 'mat-icon']);
testComponent.fontSet = 'f2';
testComponent.fontIcon = 'igloo';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['f2', 'igloo', 'mat-icon']);
testComponent.fontSet = 'f3';
testComponent.fontIcon = 'tent';
fixture.detectChanges();
expect(sortedClassNames(matIconElement)).toEqual(['f3', 'mat-icon', 'tent']);
});
});
/** Marks an svg icon url as explicitly trusted. */
function trust(iconUrl: string): SafeResourceUrl {
return sanitizer.bypassSecurityTrustResourceUrl(iconUrl);
}
});
describe('MatIcon without HttpClientModule', () => {
let iconRegistry: MatIconRegistry;
let sanitizer: DomSanitizer;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatIconModule],
declarations: [IconFromSvgName],
});
TestBed.compileComponents();
}));
beforeEach(inject([MatIconRegistry, DomSanitizer], (mir: MatIconRegistry, ds: DomSanitizer) => {
iconRegistry = mir;
sanitizer = ds;
}));
it('should throw an error when trying to load a remote icon', async() => {
const expectedError = wrappedErrorMessage(getMatIconNoHttpProviderError());
expect(() => {
iconRegistry.addSvgIcon('fido', sanitizer.bypassSecurityTrustResourceUrl('dog.svg'));
let fixture = TestBed.createComponent(IconFromSvgName);
fixture.componentInstance.iconName = 'fido';
fixture.detectChanges();
}).toThrowError(expectedError);
});
});
({template: `<mat-icon>{{iconName}}</mat-icon>`})
class IconWithLigature {
iconName = '';
}
({template: `<mat-icon [color]="iconColor">{{iconName}}</mat-icon>`})
class IconWithColor {
iconName = '';
iconColor = 'primary';
}
({template: `<mat-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></mat-icon>`})
class IconWithCustomFontCss {
fontSet = '';
fontIcon = '';
}
({template: `<mat-icon [svgIcon]="iconName"></mat-icon>`})
class IconFromSvgName {
iconName: string | undefined = '';
}
({template: '<mat-icon aria-hidden="false">face</mat-icon>'})
class IconWithAriaHiddenFalse { }
({template: `<mat-icon [svgIcon]="iconName" *ngIf="showIcon">{{iconName}}</mat-icon>`})
class IconWithBindingAndNgIf {
iconName = 'fluffy';
showIcon = true;
}