1564 lines
59 KiB
TypeScript
1564 lines
59 KiB
TypeScript
import {wrappedErrorMessage} from '@angular/cdk/testing/private';
|
|
import {
|
|
HttpClientTestingModule,
|
|
HttpTestingController,
|
|
TestRequest,
|
|
} from '@angular/common/http/testing';
|
|
import {Component, ErrorHandler, Provider, Type, ViewChild} from '@angular/core';
|
|
import {TestBed, fakeAsync, inject, tick, waitForAsync} from '@angular/core/testing';
|
|
import {DomSanitizer, SafeHtml, SafeResourceUrl} from '@angular/platform-browser';
|
|
import {FAKE_SVGS} from './fake-svgs';
|
|
import {MatIcon} from './icon';
|
|
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
|
|
import {MAT_ICON_DEFAULT_OPTIONS, MAT_ICON_LOCATION, MatIconModule} from './index';
|
|
|
|
/** 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);
|
|
}
|
|
|
|
/** Creates a test component fixture. */
|
|
function createComponent<T>(component: Type<T>, providers: Provider[] = []) {
|
|
TestBed.configureTestingModule({
|
|
imports: [MatIconModule, component],
|
|
providers: [...providers],
|
|
});
|
|
|
|
return TestBed.createComponent<T>(component);
|
|
}
|
|
|
|
describe('MatIcon', () => {
|
|
let fakePath: string;
|
|
let iconRegistry: MatIconRegistry;
|
|
let http: HttpTestingController;
|
|
let sanitizer: DomSanitizer;
|
|
let errorHandler: ErrorHandler;
|
|
|
|
beforeEach(waitForAsync(() => {
|
|
// The $ prefix tells Karma not to try to process the
|
|
// request so that we don't get warnings in our logs.
|
|
fakePath = '/$fake-path';
|
|
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
HttpClientTestingModule,
|
|
MatIconModule,
|
|
IconWithColor,
|
|
IconWithLigature,
|
|
IconWithLigatureByAttribute,
|
|
IconWithCustomFontCss,
|
|
IconFromSvgName,
|
|
IconWithAriaHiddenFalse,
|
|
IconWithBindingAndNgIf,
|
|
InlineIcon,
|
|
SvgIconWithUserContent,
|
|
IconWithLigatureAndSvgBinding,
|
|
BlankIcon,
|
|
],
|
|
providers: [
|
|
{
|
|
provide: MAT_ICON_LOCATION,
|
|
useValue: {getPathname: () => fakePath},
|
|
},
|
|
],
|
|
});
|
|
|
|
iconRegistry = TestBed.inject(MatIconRegistry);
|
|
http = TestBed.inject(HttpTestingController);
|
|
sanitizer = TestBed.inject(DomSanitizer);
|
|
errorHandler = TestBed.inject(ErrorHandler);
|
|
}));
|
|
|
|
it('should apply the correct role', () => {
|
|
const fixture = TestBed.createComponent(IconWithColor);
|
|
|
|
const icon = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
expect(icon.getAttribute('role')).toBe('img');
|
|
});
|
|
|
|
it('should include notranslate class by default', () => {
|
|
const fixture = TestBed.createComponent(IconWithColor);
|
|
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
expect(matIconElement.classList.contains('notranslate'))
|
|
.withContext('Expected the mat-icon element to include the notranslate class')
|
|
.toBeTruthy();
|
|
});
|
|
|
|
it('should apply class based on color attribute', () => {
|
|
const fixture = TestBed.createComponent(IconWithColor);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
testComponent.iconColor = 'primary';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-ligature-font',
|
|
'mat-primary',
|
|
'material-icons',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should apply a class if there is no color', () => {
|
|
const fixture = TestBed.createComponent(IconWithColor);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
testComponent.iconColor = '';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'material-icons',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
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'))
|
|
.withContext('Expected the mat-icon element has aria-hidden="true" by default')
|
|
.toBe('true');
|
|
});
|
|
|
|
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'))
|
|
.withContext('Expected the mat-icon element has the user-provided aria-hidden value')
|
|
.toBe('false');
|
|
});
|
|
|
|
it('should apply inline styling', () => {
|
|
const fixture = TestBed.createComponent(InlineIcon);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
expect(iconElement.classList.contains('mat-icon-inline'))
|
|
.withContext('Expected the mat-icon element to not include the inline styling class')
|
|
.toBeFalsy();
|
|
|
|
fixture.debugElement.componentInstance.inline = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList.contains('mat-icon-inline'))
|
|
.withContext('Expected the mat-icon element to include the inline styling class')
|
|
.toBeTruthy();
|
|
});
|
|
|
|
describe('Ligature icons', () => {
|
|
it('should add material-icons and mat-ligature-font class by default', () => {
|
|
const fixture = TestBed.createComponent(IconWithLigature);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'material-icons',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should use alternate icon font if set', () => {
|
|
iconRegistry.setDefaultFontSetClass('myfont', 'mat-ligature-font');
|
|
|
|
const fixture = TestBed.createComponent(IconWithLigature);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'myfont',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should not clear the text of a ligature icon if the svgIcon is bound to something falsy', () => {
|
|
const fixture = TestBed.createComponent(IconWithLigatureAndSvgBinding);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = undefined;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(matIconElement.textContent.trim()).toBe('house');
|
|
});
|
|
|
|
it('should be able to provide multiple alternate icon set classes', () => {
|
|
iconRegistry.setDefaultFontSetClass('myfont', 'mat-ligature-font', 'myfont-48x48');
|
|
|
|
let fixture = TestBed.createComponent(IconWithLigature);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'myfont',
|
|
'myfont-48x48',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Ligature icons by attribute', () => {
|
|
it('should forward the fontIcon attribute', () => {
|
|
const fixture = TestBed.createComponent(IconWithLigatureByAttribute);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const icon = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(icon.getAttribute('fontIcon')).toBe('home');
|
|
|
|
testComponent.iconName = 'house';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(icon.getAttribute('fontIcon')).toBe('house');
|
|
});
|
|
|
|
it('should add material-icons and mat-ligature-font class by default', () => {
|
|
const fixture = TestBed.createComponent(IconWithLigatureByAttribute);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'material-icons',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should use alternate icon font if set', () => {
|
|
iconRegistry.setDefaultFontSetClass('myfont', 'mat-ligature-font');
|
|
|
|
const fixture = TestBed.createComponent(IconWithLigatureByAttribute);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'myfont',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should be able to provide multiple alternate icon set classes', () => {
|
|
iconRegistry.setDefaultFontSetClass('myfont', 'mat-ligature-font', 'myfont-48x48');
|
|
|
|
let fixture = TestBed.createComponent(IconWithLigatureByAttribute);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
testComponent.iconName = 'home';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'mat-ligature-font',
|
|
'myfont',
|
|
'myfont-48x48',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Icons from URLs', () => {
|
|
it('should register icon URLs by name', fakeAsync(() => {
|
|
iconRegistry.addSvgIcon('fluffy', trustUrl('cat.svg'));
|
|
iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));
|
|
iconRegistry.addSvgIcon('felix', trustUrl('auth-cat.svg'), {withCredentials: true});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
let testRequest: TestRequest;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
|
|
// Change the icon, and the SVG element should be replaced.
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'meow');
|
|
|
|
// Using an icon from a previously loaded URL should not cause another HTTP request.
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectNone('dog.svg');
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
|
|
// Change icon to one that needs credentials during fetch.
|
|
testComponent.iconName = 'felix';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
testRequest = http.expectOne('auth-cat.svg');
|
|
expect(testRequest.request.withCredentials).toBeTrue();
|
|
testRequest.flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'meow');
|
|
|
|
// Assert that a registered icon can be looked-up by url.
|
|
iconRegistry.getSvgIconFromUrl(trustUrl('cat.svg')).subscribe(element => {
|
|
verifyPathChildElement(element, 'meow');
|
|
});
|
|
|
|
tick();
|
|
}));
|
|
|
|
it('should be able to set the viewBox when registering a single SVG icon', fakeAsync(() => {
|
|
iconRegistry.addSvgIcon('fluffy', trustUrl('cat.svg'), {viewBox: '0 0 27 27'});
|
|
iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'), {viewBox: '0 0 43 43'});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
|
|
|
|
// Change the icon, and the SVG element should be replaced.
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 27 27');
|
|
}));
|
|
|
|
it('should throw an error when using an untrusted icon url', () => {
|
|
iconRegistry.addSvgIcon('fluffy', 'farm-set-1.svg');
|
|
|
|
expect(() => {
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
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(() => {
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).toThrowError(/unsafe value used in a resource URL context/);
|
|
});
|
|
|
|
it('should delegate http error logging to the ErrorHandler', () => {
|
|
const handleErrorSpy = spyOn(errorHandler, 'handleError');
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
|
|
testComponent.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('farm-set-1.svg').error(new ErrorEvent('Network error'));
|
|
fixture.detectChanges();
|
|
|
|
// Called twice once for the HTTP request failing and once for the icon
|
|
// then not being able to be found.
|
|
expect(handleErrorSpy).toHaveBeenCalledTimes(2);
|
|
expect(handleErrorSpy.calls.argsFor(0)[0].message).toEqual(
|
|
'Loading icon set URL: farm-set-1.svg failed: Http failure response ' +
|
|
'for farm-set-1.svg: 0 ',
|
|
);
|
|
expect(handleErrorSpy.calls.argsFor(1)[0].message).toEqual(
|
|
`Error retrieving icon ${testComponent.iconName}! ` +
|
|
'Unable to find icon with the name "pig"',
|
|
);
|
|
});
|
|
|
|
it('should delegate an error getting an SVG icon to the ErrorHandler', () => {
|
|
const handleErrorSpy = spyOn(errorHandler, 'handleError');
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
|
|
testComponent.iconName = 'farm:DNE';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
|
|
fixture.detectChanges();
|
|
|
|
// The HTTP request succeeded but the icon was not found so we logged.
|
|
expect(handleErrorSpy).toHaveBeenCalledTimes(1);
|
|
expect(handleErrorSpy.calls.argsFor(0)[0].message).toEqual(
|
|
`Error retrieving icon ${testComponent.iconName}! ` +
|
|
'Unable to find icon with the name "DNE"',
|
|
);
|
|
});
|
|
|
|
it('should extract icon from SVG icon set', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
let svgChild: any;
|
|
|
|
testComponent.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
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.changeDetectorRef.markForCheck();
|
|
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 handle unescape characters in icon names', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-4.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
let svgChild: any;
|
|
|
|
testComponent.iconName = 'farm:pig with spaces';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('farm-set-4.svg').flush(FAKE_SVGS.farmSet4);
|
|
|
|
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');
|
|
});
|
|
|
|
it('should never parse the same icon set multiple times', () => {
|
|
// Normally we avoid spying on private methods like this, but the parsing is a private
|
|
// implementation detail that should not be exposed to the public API. This test, though,
|
|
// is important enough to warrant the brittle-ness that results.
|
|
spyOn(iconRegistry, '_svgElementFromString' as any).and.callThrough();
|
|
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
|
|
// Requests for icons must be subscribed to in order for requests to be made.
|
|
iconRegistry.getNamedSvgIcon('pig', 'farm').subscribe(() => {});
|
|
iconRegistry.getNamedSvgIcon('cow', 'farm').subscribe(() => {});
|
|
|
|
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
|
|
|
|
// _svgElementFromString is called once for each icon to create an empty SVG element
|
|
// and once to parse the full icon set.
|
|
expect((iconRegistry as any)._svgElementFromString).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('should allow multiple icon sets in a namespace', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-2.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
let svgChild: any;
|
|
|
|
testComponent.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
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')).toBeFalsy();
|
|
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.changeDetectorRef.markForCheck();
|
|
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 clear the id attribute from the svg node', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-1.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
|
|
fixture.componentInstance.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
|
|
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
const svgElement = verifyAndGetSingleSvgChild(matIconElement);
|
|
|
|
expect(svgElement.hasAttribute('id')).toBe(false);
|
|
});
|
|
|
|
it('should unwrap <symbol> nodes', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('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.changeDetectorRef.markForCheck();
|
|
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 copy over the attributes when unwrapping <symbol> nodes', () => {
|
|
iconRegistry.addSvgIconSetInNamespace('farm', trustUrl('farm-set-5.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'farm:duck';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('farm-set-5.svg').flush(FAKE_SVGS.farmSet5);
|
|
|
|
const svgElement = verifyAndGetSingleSvgChild(matIconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 13 37');
|
|
expect(svgElement.getAttribute('id')).toBeFalsy();
|
|
expect(svgElement.querySelector('symbol')).toBeFalsy();
|
|
});
|
|
|
|
it('should not wrap <svg> elements in icon sets in another svg tag', () => {
|
|
iconRegistry.addSvgIconSet(trustUrl('arrow-set.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
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(trustUrl('arrow-set.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
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.changeDetectorRef.markForCheck();
|
|
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.changeDetectorRef.markForCheck();
|
|
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', trustUrl('cat.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconWithBindingAndNgIf);
|
|
|
|
fixture.detectChanges();
|
|
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
|
|
|
|
expect(() => {
|
|
fixture.componentInstance.showIcon = false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
fixture.componentInstance.showIcon = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should be able to configure the viewBox for the icon set', () => {
|
|
iconRegistry.addSvgIconSet(trustUrl('arrow-set.svg'), {viewBox: '0 0 43 43'});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
|
|
svgElement = verifyAndGetSingleSvgChild(matIconElement);
|
|
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
|
|
});
|
|
|
|
it('should remove the SVG element from the DOM when the binding is cleared', () => {
|
|
iconRegistry.addSvgIconSet(trustUrl('arrow-set.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
|
|
const testComponent = fixture.componentInstance;
|
|
const icon = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
|
|
|
|
expect(icon.querySelector('svg')).toBeTruthy();
|
|
|
|
testComponent.iconName = undefined;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(icon.querySelector('svg')).toBeFalsy();
|
|
});
|
|
|
|
it('should keep non-SVG user content inside the icon element', fakeAsync(() => {
|
|
iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));
|
|
|
|
const fixture = TestBed.createComponent(SvgIconWithUserContent);
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
|
|
const userDiv = iconElement.querySelector('div');
|
|
|
|
expect(userDiv).toBeTruthy();
|
|
expect(iconElement.textContent.trim()).toContain('Hello');
|
|
|
|
tick();
|
|
}));
|
|
|
|
it('should cancel in-progress fetches if the icon changes', fakeAsync(() => {
|
|
// Register an icon that will resolve immediately.
|
|
iconRegistry.addSvgIconLiteral('fluffy', trustHtml(FAKE_SVGS.cat));
|
|
|
|
// Register a different icon that takes some time to resolve.
|
|
iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
// Assign the slow icon first.
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
// Assign the quick icon while the slow one is still in-flight.
|
|
fixture.componentInstance.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
// Expect for the in-flight request to have been cancelled.
|
|
expect(http.expectOne('dog.svg').cancelled).toBe(true);
|
|
|
|
// Expect the last icon to have been assigned.
|
|
verifyPathChildElement(verifyAndGetSingleSvgChild(iconElement), 'meow');
|
|
}));
|
|
|
|
it('should cancel in-progress fetches if the component is destroyed', fakeAsync(() => {
|
|
iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(http.expectOne('dog.svg').cancelled).toBe(true);
|
|
}));
|
|
});
|
|
|
|
describe('Icons from HTML string', () => {
|
|
it('should register icon HTML strings by name', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral('fluffy', trustHtml(FAKE_SVGS.cat));
|
|
iconRegistry.addSvgIconLiteral('fido', trustHtml(FAKE_SVGS.dog));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'meow');
|
|
|
|
// Assert that a registered icon can be looked-up by name.
|
|
iconRegistry.getNamedSvgIcon('fluffy').subscribe(element => {
|
|
verifyPathChildElement(element, 'meow');
|
|
});
|
|
|
|
tick();
|
|
}));
|
|
|
|
it('should be able to configure the icon viewBox', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral('fluffy', trustHtml(FAKE_SVGS.cat), {viewBox: '0 0 43 43'});
|
|
iconRegistry.addSvgIconLiteral('fido', trustHtml(FAKE_SVGS.dog), {viewBox: '0 0 27 27'});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 27 27');
|
|
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
|
|
}));
|
|
|
|
it('should throw an error when using untrusted HTML', () => {
|
|
// Stub out console.warn so we don't pollute our logs with Angular's warnings.
|
|
// Jasmine will tear the spy down at the end of the test.
|
|
spyOn(console, 'warn');
|
|
|
|
expect(() => {
|
|
iconRegistry.addSvgIconLiteral('circle', '<svg><circle></svg>');
|
|
}).toThrowError(/was not trusted as safe HTML/);
|
|
});
|
|
|
|
it('should extract an icon from SVG icon set', () => {
|
|
iconRegistry.addSvgIconSetLiteralInNamespace('farm', trustHtml(FAKE_SVGS.farmSet1));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
let svgChild: any;
|
|
|
|
testComponent.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
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.changeDetectorRef.markForCheck();
|
|
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.addSvgIconSetLiteralInNamespace('farm', trustHtml(FAKE_SVGS.farmSet1));
|
|
iconRegistry.addSvgIconSetLiteralInNamespace('farm', trustHtml(FAKE_SVGS.farmSet2));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
let svgChild: any;
|
|
|
|
testComponent.iconName = 'farm:pig';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
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')).toBeFalsy();
|
|
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.changeDetectorRef.markForCheck();
|
|
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 return unmodified copies of icons from icon sets', () => {
|
|
iconRegistry.addSvgIconSetLiteral(trustHtml(FAKE_SVGS.arrows));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
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.changeDetectorRef.markForCheck();
|
|
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.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(matIconElement);
|
|
verifyPathChildElement(svgElement, 'left');
|
|
expect(svgElement.getAttribute('viewBox')).toBeFalsy();
|
|
});
|
|
|
|
it('should be able to configure the viewBox for the icon set', () => {
|
|
iconRegistry.addSvgIconSetLiteral(trustHtml(FAKE_SVGS.arrows), {viewBox: '0 0 43 43'});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
let svgElement: SVGElement;
|
|
|
|
testComponent.iconName = 'left-arrow';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(matIconElement);
|
|
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
|
|
});
|
|
|
|
it('should prepend the current path to attributes with `url()` references', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral(
|
|
'fido',
|
|
trustHtml(`
|
|
<svg>
|
|
<filter id="blur">
|
|
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
|
|
</filter>
|
|
|
|
<circle cx="170" cy="60" r="50" fill="green" filter="url('#blur')" />
|
|
</svg>
|
|
`),
|
|
);
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const circle = fixture.nativeElement.querySelector('mat-icon svg circle');
|
|
|
|
// We use a regex to match here, rather than the exact value, because different browsers
|
|
// return different quotes through `getAttribute`, while some even omit the quotes altogether.
|
|
expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/\$fake-path#blur['"]?\)$/);
|
|
|
|
tick();
|
|
}));
|
|
|
|
it('should use latest path when prefixing the `url()` references', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral(
|
|
'fido',
|
|
trustHtml(`
|
|
<svg>
|
|
<filter id="blur">
|
|
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
|
|
</filter>
|
|
|
|
<circle cx="170" cy="60" r="50" fill="green" filter="url('#blur')" />
|
|
</svg>
|
|
`),
|
|
);
|
|
|
|
let fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
let circle = fixture.nativeElement.querySelector('mat-icon svg circle');
|
|
|
|
expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/\$fake-path#blur['"]?\)$/);
|
|
tick();
|
|
fixture.destroy();
|
|
|
|
fakePath = '/$another-fake-path';
|
|
fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
circle = fixture.nativeElement.querySelector('mat-icon svg circle');
|
|
|
|
expect(circle.getAttribute('filter')).toMatch(
|
|
/^url\(['"]?\/\$another-fake-path#blur['"]?\)$/,
|
|
);
|
|
tick();
|
|
}));
|
|
|
|
it('should update the `url()` references when the path changes', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral(
|
|
'fido',
|
|
trustHtml(`
|
|
<svg>
|
|
<filter id="blur">
|
|
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
|
|
</filter>
|
|
|
|
<circle cx="170" cy="60" r="50" fill="green" filter="url('#blur')" />
|
|
</svg>
|
|
`),
|
|
);
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const circle = fixture.nativeElement.querySelector('mat-icon svg circle');
|
|
|
|
// We use a regex to match here, rather than the exact value, because different browsers
|
|
// return different quotes through `getAttribute`, while some even omit the quotes altogether.
|
|
expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/\$fake-path#blur['"]?\)$/);
|
|
tick();
|
|
|
|
fakePath = '/$different-path';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/\$different-path#blur['"]?\)$/);
|
|
}));
|
|
});
|
|
|
|
describe('custom fonts', () => {
|
|
it('should apply CSS classes for custom font and icon', () => {
|
|
iconRegistry.registerFontClassAlias('f1', 'font1');
|
|
iconRegistry.registerFontClassAlias('f2');
|
|
|
|
const fixture = TestBed.createComponent(IconWithCustomFontCss);
|
|
const testComponent = fixture.componentInstance;
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.fontSet = 'f1';
|
|
testComponent.fontIcon = 'house';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'font1',
|
|
'house',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
|
|
testComponent.fontSet = 'f2';
|
|
testComponent.fontIcon = 'igloo';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'f2',
|
|
'igloo',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
|
|
testComponent.fontSet = 'f3';
|
|
testComponent.fontIcon = 'tent';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'f3',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
'tent',
|
|
]);
|
|
});
|
|
|
|
it('should handle values with extraneous spaces being passed in to `fontSet`', () => {
|
|
const fixture = TestBed.createComponent(IconWithCustomFontCss);
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
expect(() => {
|
|
fixture.componentInstance.fontSet = 'font set';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'font',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
|
|
expect(() => {
|
|
fixture.componentInstance.fontSet = ' changed';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'changed',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
|
|
it('should handle values with extraneous spaces being passed in to `fontIcon`', () => {
|
|
const fixture = TestBed.createComponent(IconWithCustomFontCss);
|
|
const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
fixture.componentInstance.fontSet = 'f1';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
|
|
expect(() => {
|
|
fixture.componentInstance.fontIcon = 'font icon';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'f1',
|
|
'font',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
|
|
expect(() => {
|
|
fixture.componentInstance.fontIcon = ' changed';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
|
|
expect(sortedClassNames(matIconElement)).toEqual([
|
|
'changed',
|
|
'f1',
|
|
'mat-icon',
|
|
'mat-icon-no-color',
|
|
'notranslate',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Icons resolved through a resolver function', () => {
|
|
it('should resolve icons through a resolver function', fakeAsync(() => {
|
|
iconRegistry.addSvgIconResolver(name => {
|
|
if (name === 'fluffy') {
|
|
return trustUrl('cat.svg');
|
|
} else if (name === 'fido') {
|
|
return trustUrl('dog.svg');
|
|
} else if (name === 'felix') {
|
|
return {url: trustUrl('auth-cat.svg'), options: {withCredentials: true}};
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
let testRequest: TestRequest;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
|
|
// Change the icon, and the SVG element should be replaced.
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'meow');
|
|
|
|
// Using an icon from a previously loaded URL should not cause another HTTP request.
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectNone('dog.svg');
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
|
|
// Change icon to one that needs credentials during fetch.
|
|
testComponent.iconName = 'felix';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
testRequest = http.expectOne('auth-cat.svg');
|
|
expect(testRequest.request.withCredentials).toBeTrue();
|
|
testRequest.flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'meow');
|
|
|
|
// Assert that a registered icon can be looked-up by url.
|
|
iconRegistry.getSvgIconFromUrl(trustUrl('cat.svg')).subscribe(element => {
|
|
verifyPathChildElement(element, 'meow');
|
|
});
|
|
|
|
tick();
|
|
}));
|
|
|
|
it('should fall back to second resolver if the first one returned null', fakeAsync(() => {
|
|
iconRegistry
|
|
.addSvgIconResolver(() => null)
|
|
.addSvgIconResolver(name => (name === 'fido' ? trustUrl('dog.svg') : null));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
verifyPathChildElement(verifyAndGetSingleSvgChild(iconElement), 'woof');
|
|
tick();
|
|
}));
|
|
|
|
it('should be able to set the viewBox when resolving an icon with a function', fakeAsync(() => {
|
|
iconRegistry.addSvgIconResolver(name => {
|
|
if (name === 'fluffy') {
|
|
return {url: trustUrl('cat.svg'), options: {viewBox: '0 0 27 27'}};
|
|
} else if (name === 'fido') {
|
|
return {url: trustUrl('dog.svg'), options: {viewBox: '0 0 43 43'}};
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
let svgElement: SVGElement;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('dog.svg').flush(FAKE_SVGS.dog);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
|
|
|
|
// Change the icon, and the SVG element should be replaced.
|
|
testComponent.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
http.expectOne('cat.svg').flush(FAKE_SVGS.cat);
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
expect(svgElement.getAttribute('viewBox')).toBe('0 0 27 27');
|
|
}));
|
|
|
|
it('should throw an error when the resolver returns an untrusted URL', () => {
|
|
iconRegistry.addSvgIconResolver(() => 'not-trusted.svg');
|
|
|
|
expect(() => {
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
fixture.componentInstance.iconName = 'fluffy';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).toThrowError(/unsafe value used in a resource URL context/);
|
|
});
|
|
});
|
|
|
|
it('should handle assigning an icon through the setter', fakeAsync(() => {
|
|
iconRegistry.addSvgIconLiteral('fido', trustHtml(FAKE_SVGS.dog));
|
|
|
|
const fixture = TestBed.createComponent(BlankIcon);
|
|
fixture.detectChanges();
|
|
let svgElement: SVGElement;
|
|
const testComponent = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
|
|
testComponent.icon.svgIcon = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
svgElement = verifyAndGetSingleSvgChild(iconElement);
|
|
verifyPathChildElement(svgElement, 'woof');
|
|
tick();
|
|
}));
|
|
|
|
/** Marks an SVG icon url as explicitly trusted. */
|
|
function trustUrl(iconUrl: string): SafeResourceUrl {
|
|
return sanitizer.bypassSecurityTrustResourceUrl(iconUrl);
|
|
}
|
|
|
|
/** Marks an SVG icon string as explicitly trusted. */
|
|
function trustHtml(iconHtml: string): SafeHtml {
|
|
return sanitizer.bypassSecurityTrustHtml(iconHtml);
|
|
}
|
|
});
|
|
|
|
describe('MatIcon without HttpClientModule', () => {
|
|
let iconRegistry: MatIconRegistry;
|
|
let sanitizer: DomSanitizer;
|
|
|
|
@Component({
|
|
template: `<mat-icon [svgIcon]="iconName"></mat-icon>`,
|
|
standalone: false,
|
|
})
|
|
class IconFromSvgName {
|
|
iconName: string | undefined = '';
|
|
}
|
|
|
|
beforeEach(waitForAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [MatIconModule],
|
|
declarations: [IconFromSvgName],
|
|
});
|
|
}));
|
|
|
|
beforeEach(inject([MatIconRegistry, DomSanitizer], (mir: MatIconRegistry, ds: DomSanitizer) => {
|
|
iconRegistry = mir;
|
|
sanitizer = ds;
|
|
}));
|
|
|
|
it('should throw an error when trying to load a remote icon', () => {
|
|
const expectedError = wrappedErrorMessage(getMatIconNoHttpProviderError());
|
|
|
|
expect(() => {
|
|
iconRegistry.addSvgIcon('fido', sanitizer.bypassSecurityTrustResourceUrl('dog.svg'));
|
|
|
|
const fixture = TestBed.createComponent(IconFromSvgName);
|
|
|
|
fixture.componentInstance.iconName = 'fido';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
}).toThrowError(expectedError);
|
|
});
|
|
});
|
|
|
|
describe('MatIcon with default options', () => {
|
|
it('should be able to configure color globally', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithLigature, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'accent'}},
|
|
]);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).not.toContain('mat-icon-no-color');
|
|
expect(iconElement.classList).toContain('mat-accent');
|
|
}));
|
|
|
|
it('should use passed color rather then color provided', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithColor, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'warn'}},
|
|
]);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).not.toContain('mat-warn');
|
|
expect(iconElement.classList).toContain('mat-primary');
|
|
}));
|
|
|
|
it('should use default color if no color passed', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithColor, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'accent'}},
|
|
]);
|
|
const component = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
component.iconColor = '';
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).not.toContain('mat-icon-no-color');
|
|
expect(iconElement.classList).not.toContain('mat-primary');
|
|
expect(iconElement.classList).toContain('mat-accent');
|
|
}));
|
|
|
|
it('should be able to configure font set globally', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithLigature, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'custom-font-set'}},
|
|
]);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).toContain('custom-font-set');
|
|
}));
|
|
|
|
it('should use passed fontSet rather then default one', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithCustomFontCss, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'default-font-set'}},
|
|
]);
|
|
const component = fixture.componentInstance;
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
component.fontSet = 'custom-font-set';
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).not.toContain('default-font-set');
|
|
expect(iconElement.classList).toContain('custom-font-set');
|
|
}));
|
|
|
|
it('should use passed empty fontSet rather then default one', fakeAsync(() => {
|
|
const fixture = createComponent(IconWithCustomFontCss, [
|
|
{provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'default-font-set'}},
|
|
]);
|
|
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
|
|
fixture.detectChanges();
|
|
expect(iconElement.classList).not.toContain('default-font-set');
|
|
}));
|
|
});
|
|
|
|
@Component({
|
|
template: `<mat-icon>{{iconName}}</mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithLigature {
|
|
iconName = '';
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [fontIcon]="iconName"></mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithLigatureByAttribute {
|
|
iconName = '';
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [color]="iconColor">{{iconName}}</mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithColor {
|
|
iconName = '';
|
|
iconColor = 'primary';
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithCustomFontCss {
|
|
fontSet = '';
|
|
fontIcon = '';
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [svgIcon]="iconName"></mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconFromSvgName {
|
|
iconName: string | undefined = '';
|
|
}
|
|
|
|
@Component({
|
|
template: '<mat-icon aria-hidden="false">face</mat-icon>',
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithAriaHiddenFalse {}
|
|
|
|
@Component({
|
|
template: `@if (showIcon) {<mat-icon [svgIcon]="iconName">{{iconName}}</mat-icon>}`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithBindingAndNgIf {
|
|
iconName = 'fluffy';
|
|
showIcon = true;
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [inline]="inline">{{iconName}}</mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class InlineIcon {
|
|
inline = false;
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon [svgIcon]="iconName"><div>Hello</div></mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class SvgIconWithUserContent {
|
|
iconName: string | undefined = '';
|
|
}
|
|
|
|
@Component({
|
|
template: '<mat-icon [svgIcon]="iconName">house</mat-icon>',
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class IconWithLigatureAndSvgBinding {
|
|
iconName: string | undefined;
|
|
}
|
|
|
|
@Component({
|
|
template: `<mat-icon></mat-icon>`,
|
|
standalone: true,
|
|
imports: [HttpClientTestingModule, MatIconModule],
|
|
})
|
|
class BlankIcon {
|
|
@ViewChild(MatIcon) icon: MatIcon;
|
|
}
|