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;
}
