https://github.com/angular/angular
Raw File
Tip revision: 230b98d4ddffee4df25f62ff3c9ac39a2329abf6 authored by Tobias Bosch on 19 October 2017, 00:22:45 UTC
docs: add changelog for 5.0.0-rc.3
Tip revision: 230b98d
app.component.spec.ts
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { MdProgressBar, MdSidenav } from '@angular/material';
import { By } from '@angular/platform-browser';

import { of } from 'rxjs/observable/of';

import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service';
import { GaService } from 'app/shared/ga.service';
import { LocationService } from 'app/shared/location.service';
import { Logger } from 'app/shared/logger.service';
import { MockLocationService } from 'testing/location.service';
import { MockLogger } from 'testing/logger.service';
import { MockSearchService } from 'testing/search.service';
import { NavigationNode } from 'app/navigation/navigation.service';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { SelectComponent } from 'app/shared/select/select.component';
import { TocComponent } from 'app/embedded/toc/toc.component';
import { TocItem, TocService } from 'app/shared/toc.service';

const sideBySideBreakPoint = 992;
const hideToCBreakPoint = 800;

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

  let docViewer: HTMLElement;
  let hamburger: HTMLButtonElement;
  let locationService: MockLocationService;
  let sidenav: HTMLElement;
  let tocService: TocService;

  const initializeTest = () => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;

    fixture.detectChanges();
    component.onResize(sideBySideBreakPoint + 1); // wide by default

    const de = fixture.debugElement;
    docViewer = de.query(By.css('aio-doc-viewer')).nativeElement;
    hamburger = de.query(By.css('.hamburger')).nativeElement;
    locationService = de.injector.get(LocationService) as any as MockLocationService;
    sidenav = de.query(By.css('md-sidenav')).nativeElement;
    tocService = de.injector.get(TocService);
  };

  describe('with proper DocViewer', () => {

    beforeEach(() => {
      createTestingModule('a/b');
      initializeTest();
    });

    it('should create', () => {
      expect(component).toBeDefined();
    });

    describe('hasFloatingToc', () => {
      it('should initially be true', () => {
        const fixture2 = TestBed.createComponent(AppComponent);
        const component2 = fixture2.componentInstance;

        expect(component2.hasFloatingToc).toBe(true);
      });

      it('should be false on narrow screens', () => {
        component.onResize(hideToCBreakPoint - 1);

        tocService.tocList.next([{}, {}, {}] as TocItem[]);
        expect(component.hasFloatingToc).toBe(false);

        tocService.tocList.next([]);
        expect(component.hasFloatingToc).toBe(false);

        tocService.tocList.next([{}, {}, {}] as TocItem[]);
        expect(component.hasFloatingToc).toBe(false);
      });

      it('should be true on wide screens unless the toc is empty', () => {
        component.onResize(hideToCBreakPoint + 1);

        tocService.tocList.next([{}, {}, {}] as TocItem[]);
        expect(component.hasFloatingToc).toBe(true);

        tocService.tocList.next([]);
        expect(component.hasFloatingToc).toBe(false);

        tocService.tocList.next([{}, {}, {}] as TocItem[]);
        expect(component.hasFloatingToc).toBe(true);
      });

      it('should be false when toc is empty', () => {
        tocService.tocList.next([]);

        component.onResize(hideToCBreakPoint + 1);
        expect(component.hasFloatingToc).toBe(false);

        component.onResize(hideToCBreakPoint - 1);
        expect(component.hasFloatingToc).toBe(false);

        component.onResize(hideToCBreakPoint + 1);
        expect(component.hasFloatingToc).toBe(false);
      });

      it('should be true when toc is not empty unless the screen is narrow', () => {
        tocService.tocList.next([{}, {}, {}] as TocItem[]);

        component.onResize(hideToCBreakPoint + 1);
        expect(component.hasFloatingToc).toBe(true);

        component.onResize(hideToCBreakPoint - 1);
        expect(component.hasFloatingToc).toBe(false);

        component.onResize(hideToCBreakPoint + 1);
        expect(component.hasFloatingToc).toBe(true);
      });
    });

    describe('isSideBySide', () => {
      it('should be updated on resize', () => {
        component.onResize(sideBySideBreakPoint - 1);
        expect(component.isSideBySide).toBe(false);

        component.onResize(sideBySideBreakPoint + 1);
        expect(component.isSideBySide).toBe(true);
      });
    });

    describe('onScroll', () => {
      it('should update `tocMaxHeight` accordingly', () => {
        expect(component.tocMaxHeight).toBeUndefined();

        component.onScroll();
        expect(component.tocMaxHeight).toBeGreaterThan(0);
      });
    });

    describe('SideNav when side-by-side (wide)', () => {

      beforeEach(() => {
        component.onResize(sideBySideBreakPoint + 1); // side-by-side
      });

      it('should open when nav to a guide page (guide/pipes)', () => {
        locationService.go('guide/pipes');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-open/);
      });

      it('should open when nav to an api page', () => {
        locationService.go('api/a/b/c/d');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-open/);
      });

      it('should be closed when nav to a marketing page (features)', () => {
        locationService.go('features');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-clos/);
      });

      describe('when manually closed', () => {

        beforeEach(() => {
          locationService.go('guide/pipes');
          fixture.detectChanges();
          hamburger.click();
          fixture.detectChanges();
        });

        it('should be closed', () => {
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should stay closed when nav from one guide page to another', () => {
          locationService.go('guide/bags');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should stay closed when nav from a guide page to api page', () => {
          locationService.go('api');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should reopen when nav to market page and back to guide page', () => {
          locationService.go('features');
          fixture.detectChanges();
          locationService.go('guide/bags');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-open/);
        });
      });
    });

    describe('SideNav when NOT side-by-side (narrow)', () => {

      beforeEach(() => {
        component.onResize(sideBySideBreakPoint - 1); // NOT side-by-side
      });

      it('should be closed when nav to a guide page (guide/pipes)', () => {
        locationService.go('guide/pipes');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-clos/);
      });

      it('should be closed when nav to an api page', () => {
        locationService.go('api/a/b/c/d');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-clos/);
      });

      it('should be closed when nav to a marketing page (features)', () => {
        locationService.go('features');
        fixture.detectChanges();
        expect(sidenav.className).toMatch(/sidenav-clos/);
      });

      describe('when manually opened', () => {

        beforeEach(() => {
          locationService.go('guide/pipes');
          fixture.detectChanges();
          hamburger.click();
          fixture.detectChanges();
        });

        it('should be open', () => {
          expect(sidenav.className).toMatch(/sidenav-open/);
        });

        it('should close when click in gray content area overlay', () => {
          const sidenavBackdrop = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement;
          sidenavBackdrop.click();
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should close when nav to another guide page', () => {
          locationService.go('guide/bags');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should close when nav to api page', () => {
          locationService.go('api');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

        it('should close again when nav to market page', () => {
          locationService.go('features');
          fixture.detectChanges();
          expect(sidenav.className).toMatch(/sidenav-clos/);
        });

      });
    });

    describe('SideNav version selector', () => {
      let selectElement: DebugElement;
      let selectComponent: SelectComponent;

      function setupSelectorForTesting(mode?: string) {
        createTestingModule('a/b', mode);
        initializeTest();
        component.onResize(sideBySideBreakPoint + 1); // side-by-side
        selectElement = fixture.debugElement.query(By.directive(SelectComponent));
        selectComponent = selectElement.componentInstance;
      }

      it('should select the version that matches the deploy mode', () => {
        setupSelectorForTesting();
        expect(selectComponent.selected.title).toContain('stable');
        setupSelectorForTesting('next');
        expect(selectComponent.selected.title).toContain('next');
        setupSelectorForTesting('archive');
        expect(selectComponent.selected.title).toContain('v4');
      });

      it('should add the current raw version string to the selected version', () => {
        setupSelectorForTesting();
        expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
        setupSelectorForTesting('next');
        expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
        setupSelectorForTesting('archive');
        expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
      });

      // Older docs versions have an href
      it('should navigate when change to a version with a url', () => {
        setupSelectorForTesting();
        const versionWithUrlIndex = component.docVersions.findIndex(v => !!v.url);
        const versionWithUrl = component.docVersions[versionWithUrlIndex];
        selectElement.triggerEventHandler('change', { option: versionWithUrl, index: versionWithUrlIndex});
        expect(locationService.go).toHaveBeenCalledWith(versionWithUrl.url);
      });

      it('should not navigate when change to a version without a url', () => {
        setupSelectorForTesting();
        const versionWithoutUrlIndex = component.docVersions.length;
        const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex] = { title: 'foo', url: null };
        selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex });
        expect(locationService.go).not.toHaveBeenCalled();
      });
    });

    describe('pageId', () => {

      it('should set the id of the doc viewer container based on the current doc', () => {
        const container = fixture.debugElement.query(By.css('section.sidenav-content'));

        locationService.go('guide/pipes');
        fixture.detectChanges();
        expect(component.pageId).toEqual('guide-pipes');
        expect(container.properties['id']).toEqual('guide-pipes');

        locationService.go('news');
        fixture.detectChanges();
        expect(component.pageId).toEqual('news');
        expect(container.properties['id']).toEqual('news');

        locationService.go('');
        fixture.detectChanges();
        expect(component.pageId).toEqual('home');
        expect(container.properties['id']).toEqual('home');
      });

      it('should not be affected by changes to the query', () => {
        const container = fixture.debugElement.query(By.css('section.sidenav-content'));

        locationService.go('guide/pipes');
        fixture.detectChanges();

        locationService.go('guide/other?search=http');
        fixture.detectChanges();
        expect(component.pageId).toEqual('guide-other');
        expect(container.properties['id']).toEqual('guide-other');
      });
    });

    describe('hostClasses', () => {

      it('should set the css classes of the host container based on the current doc and navigation view', () => {
        locationService.go('guide/pipes');
        fixture.detectChanges();

        checkHostClass('page', 'guide-pipes');
        checkHostClass('folder', 'guide');
        checkHostClass('view', 'SideNav');

        locationService.go('features');
        fixture.detectChanges();
        checkHostClass('page', 'features');
        checkHostClass('folder', 'features');
        checkHostClass('view', 'TopBar');

        locationService.go('');
        fixture.detectChanges();
        checkHostClass('page', 'home');
        checkHostClass('folder', 'home');
        checkHostClass('view', '');
      });

      it('should set the css class of the host container based on the open/closed state of the side nav', () => {
        const sideNav = fixture.debugElement.query(By.directive(MdSidenav));

        locationService.go('guide/pipes');
        fixture.detectChanges();
        checkHostClass('sidenav', 'open');

        sideNav.componentInstance.opened = false;
        sideNav.triggerEventHandler('close', {});
        fixture.detectChanges();
        checkHostClass('sidenav', 'closed');

        sideNav.componentInstance.opened = true;
        sideNav.triggerEventHandler('open', {});
        fixture.detectChanges();
        checkHostClass('sidenav', 'open');
      });

      it('should set the css class of the host container based on the initial deployment mode', () => {
        createTestingModule('a/b', 'archive');
        initializeTest();
        checkHostClass('mode', 'archive');
      });

      function checkHostClass(type, value) {
        const host = fixture.debugElement;
        const classes = host.properties['className'];
        const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
        expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
        expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
      }
    });

    describe('currentDocument', () => {

      it('should display a guide page (guide/pipes)', () => {
        locationService.go('guide/pipes');
        fixture.detectChanges();
        expect(docViewer.textContent).toMatch(/Pipes/i);
      });

      it('should display the api page', () => {
        locationService.go('api');
        fixture.detectChanges();
        expect(docViewer.textContent).toMatch(/API/i);
      });

      it('should display a marketing page', () => {
        locationService.go('features');
        fixture.detectChanges();
        expect(docViewer.textContent).toMatch(/Features/i);
      });

      it('should update the document title', () => {
        const titleService = TestBed.get(Title);
        spyOn(titleService, 'setTitle');
        locationService.go('guide/pipes');
        fixture.detectChanges();
        expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Pipes');
      });

      it('should update the document title, with a default value if the document has no title', () => {
        const titleService = TestBed.get(Title);
        spyOn(titleService, 'setTitle');
        locationService.go('no-title');
        fixture.detectChanges();
        expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
      });
    });

    describe('auto-scrolling', () => {
      const scrollDelay = 500;
      let scrollService: ScrollService;
      let scrollSpy: jasmine.Spy;

      beforeEach(() => {
        scrollService = fixture.debugElement.injector.get(ScrollService);
        scrollSpy = spyOn(scrollService, 'scroll');
      });

      it('should not scroll immediately when the docId (path) changes', () => {
        locationService.go('guide/pipes');
        // deliberately not calling `fixture.detectChanges` because don't want `onDocRendered`
        expect(scrollSpy).not.toHaveBeenCalled();
      });

      it('should scroll when just the hash changes (# alone)', () => {
        locationService.go('guide/pipes');
        locationService.go('guide/pipes#somewhere');
        expect(scrollSpy).toHaveBeenCalled();
      });

      it('should scroll when just the hash changes (/#)', () => {
        locationService.go('guide/pipes');
        locationService.go('guide/pipes/#somewhere');
        expect(scrollSpy).toHaveBeenCalled();
      });

      it('should scroll again when nav to the same hash twice in succession', () => {
        locationService.go('guide/pipes');
        locationService.go('guide/pipes#somewhere');
        locationService.go('guide/pipes#somewhere');
        expect(scrollSpy.calls.count()).toBe(2);
      });

      it('should scroll when nav to the same path', () => {
        locationService.go('guide/pipes');
        scrollSpy.calls.reset();

        locationService.go('guide/pipes');
        expect(scrollSpy).toHaveBeenCalledTimes(1);
      });

      it('should scroll when e-nav to the empty path', () => {
        locationService.go('');
        scrollSpy.calls.reset();

        locationService.go('');
        expect(scrollSpy).toHaveBeenCalledTimes(1);
      });

      it('should scroll after a delay when call onDocRendered directly', fakeAsync(() => {
        component.onDocRendered();
        expect(scrollSpy).not.toHaveBeenCalled();
        tick(scrollDelay);
        expect(scrollSpy).toHaveBeenCalled();
      }));

      it('should scroll (via onDocRendered) when finish navigating to a new doc', fakeAsync(() => {
        locationService.go('guide/pipes');
        fixture.detectChanges(); // triggers the event that calls onDocRendered
        expect(scrollSpy).not.toHaveBeenCalled();
        tick(scrollDelay);
        expect(scrollSpy).toHaveBeenCalled();
      }));
    });

    describe('click intercepting', () => {
      it('should intercept clicks on anchors and call `location.handleAnchorClick()`',
              inject([LocationService], (location: LocationService) => {

        const el = fixture.nativeElement as Element;
        el.innerHTML = '<a href="some/local/url">click me</a>';
        const anchorElement = el.getElementsByTagName('a')[0];
        anchorElement.click();
        expect(location.handleAnchorClick).toHaveBeenCalledWith(anchorElement, 0, false, false);
      }));

      it('should intercept clicks on elements deep within an anchor tag',
              inject([LocationService], (location: LocationService) => {

        const el = fixture.nativeElement as Element;
        el.innerHTML = '<a href="some/local/url"><div><img></div></a>';
        const imageElement  = el.getElementsByTagName('img')[0];
        const anchorElement = el.getElementsByTagName('a')[0];
        imageElement.click();
        expect(location.handleAnchorClick).toHaveBeenCalledWith(anchorElement, 0, false, false);
      }));

      it('should ignore clicks on elements without an anchor ancestor',
              inject([LocationService], (location: LocationService) => {

        const el = fixture.nativeElement as Element;
        el.innerHTML = '<div><p><div><img></div></p></div>';
        const imageElement  = el.getElementsByTagName('img')[0];
        imageElement.click();
        expect(location.handleAnchorClick).not.toHaveBeenCalled();
      }));
    });

    describe('restrainScrolling()', () => {
      const preventedScrolling = (currentTarget: object, deltaY: number) => {
        const evt = {
          deltaY,
          currentTarget,
          defaultPrevented: false,
          preventDefault() { this.defaultPrevented = true; }
        } as any as WheelEvent;

        component.restrainScrolling(evt);

        return evt.defaultPrevented;
      };

      it('should prevent scrolling up if already at the top', () => {
        const elem = {scrollTop: 0};

        expect(preventedScrolling(elem, -100)).toBe(true);
        expect(preventedScrolling(elem, +100)).toBe(false);
        expect(preventedScrolling(elem, -10)).toBe(true);
      });

      it('should prevent scrolling down if already at the bottom', () => {
        const elem = {scrollTop: 100, scrollHeight: 150, clientHeight: 50};

        expect(preventedScrolling(elem, +10)).toBe(true);
        expect(preventedScrolling(elem, -10)).toBe(false);
        expect(preventedScrolling(elem, +5)).toBe(true);

        elem.clientHeight -= 10;
        expect(preventedScrolling(elem, +5)).toBe(false);

        elem.scrollHeight -= 20;
        expect(preventedScrolling(elem, +5)).toBe(true);

        elem.scrollTop -= 30;
        expect(preventedScrolling(elem, +5)).toBe(false);
      });

      it('should not prevent scrolling if neither at the top nor at the bottom', () => {
        const elem = {scrollTop: 50, scrollHeight: 150, clientHeight: 50};

        expect(preventedScrolling(elem, +100)).toBe(false);
        expect(preventedScrolling(elem, -100)).toBe(false);
      });
    });

    describe('aio-toc', () => {
      let tocDebugElement: DebugElement;
      let tocContainer: DebugElement;

      const setHasFloatingToc = hasFloatingToc => {
        component.hasFloatingToc = hasFloatingToc;
        fixture.detectChanges();

        tocDebugElement = fixture.debugElement.query(By.directive(TocComponent));
        tocContainer = tocDebugElement && tocDebugElement.parent;
      };

      beforeEach(() => setHasFloatingToc(true));


      it('should show/hide `<aio-toc>` based on `hasFloatingToc`', () => {
        expect(tocDebugElement).toBeTruthy();
        expect(tocContainer).toBeTruthy();

        setHasFloatingToc(false);
        expect(tocDebugElement).toBeFalsy();
        expect(tocContainer).toBeFalsy();

        setHasFloatingToc(true);
        expect(tocDebugElement).toBeTruthy();
        expect(tocContainer).toBeTruthy();
      });

      it('should have a non-embedded `<aio-toc>` element', () => {
        expect(tocDebugElement.classes['embedded']).toBeFalsy();
      });

      it('should update the TOC container\'s `maxHeight` based on `tocMaxHeight`', () => {
        expect(tocContainer.styles['max-height']).toBeNull();

        component.tocMaxHeight = '100';
        fixture.detectChanges();

        expect(tocContainer.styles['max-height']).toBe('100px');
      });

      it('should restrain scrolling inside the ToC container', () => {
        const restrainScrolling = spyOn(component, 'restrainScrolling');
        const evt = {};

        expect(restrainScrolling).not.toHaveBeenCalled();

        tocContainer.triggerEventHandler('mousewheel', evt);
        expect(restrainScrolling).toHaveBeenCalledWith(evt);
      });
    });

    describe('footer', () => {
      it('should have version number', () => {
        const versionEl: HTMLElement = fixture.debugElement.query(By.css('aio-footer')).nativeElement;
        expect(versionEl.textContent).toContain(TestHttpClient.versionInfo.full);
      });
    });

    describe('deployment banner', () => {
      it('should show a message if the deployment mode is "archive"', () => {
        createTestingModule('a/b', 'archive');
        initializeTest();
        fixture.detectChanges();
        const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement;
        expect(banner.textContent).toContain('archived documentation for Angular v4');
      });

      it('should show no message if the deployment mode is not "archive"', () => {
        createTestingModule('a/b', 'stable');
        initializeTest();
        fixture.detectChanges();
        const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement;
        expect(banner.textContent.trim()).toEqual('');
      });
    });

    describe('search', () => {
      describe('initialization', () => {
        it('should initialize the search worker', inject([SearchService], (searchService: SearchService) => {
          fixture.detectChanges(); // triggers ngOnInit
          expect(searchService.initWorker).toHaveBeenCalled();
        }));
      });

      describe('click handling', () => {
        it('should intercept clicks not on the search elements and hide the search results', () => {
          component.showSearchResults = true;
          fixture.detectChanges();
          // docViewer is a commonly-clicked, non-search element
          docViewer.click();
          expect(component.showSearchResults).toBe(false);
        });

        it('should not intercept clicks on the searchResults', () => {
          component.showSearchResults = true;
          fixture.detectChanges();

          const searchResults = fixture.debugElement.query(By.directive(SearchResultsComponent));
          searchResults.nativeElement.click();
          fixture.detectChanges();

          expect(component.showSearchResults).toBe(true);
        });

        it('should not intercept clicks om the searchBox', () => {
          component.showSearchResults = true;
          fixture.detectChanges();

          const searchBox = fixture.debugElement.query(By.directive(SearchBoxComponent));
          searchBox.nativeElement.click();
          fixture.detectChanges();

          expect(component.showSearchResults).toBe(true);
        });
      });

      describe('keyup handling', () => {
        it('should grab focus when the / key is pressed', () => {
          const searchBox: SearchBoxComponent = fixture.debugElement.query(By.directive(SearchBoxComponent)).componentInstance;
          spyOn(searchBox, 'focus');
          window.document.dispatchEvent(new KeyboardEvent('keyup', { 'key': '/' }));
          fixture.detectChanges();
          expect(searchBox.focus).toHaveBeenCalled();
        });

        it('should set focus back to the search box when the search results are displayed and the escape key is pressed', () => {
          const searchBox: SearchBoxComponent = fixture.debugElement.query(By.directive(SearchBoxComponent)).componentInstance;
          spyOn(searchBox, 'focus');
          component.showSearchResults = true;
          window.document.dispatchEvent(new KeyboardEvent('keyup', { 'key': 'Escape' }));
          fixture.detectChanges();
          expect(searchBox.focus).toHaveBeenCalled();
        });
      });

      describe('showing search results', () => {
        it('should not display search results when query is empty', () => {
          const searchService: MockSearchService = TestBed.get(SearchService);
          searchService.searchResults.next({ query: '', results: [] });
          fixture.detectChanges();
          expect(component.showSearchResults).toBe(false);
        });

        it('should hide the results when a search result is selected', () => {
          const searchService: MockSearchService = TestBed.get(SearchService);

          const results = [
            { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
          ];

          searchService.searchResults.next({ query: 'something', results: results });
          component.showSearchResults = true;
          fixture.detectChanges();

          const searchResultsComponent = fixture.debugElement.query(By.directive(SearchResultsComponent));
          searchResultsComponent.triggerEventHandler('resultSelected', {});
          fixture.detectChanges();
          expect(component.showSearchResults).toBe(false);
        });

        it('should re-run the search when the search box regains focus', () => {
          const doSearchSpy = spyOn(component, 'doSearch');
          const searchBox = fixture.debugElement.query(By.directive(SearchBoxComponent));
          searchBox.triggerEventHandler('onFocus', 'some query');
          expect(doSearchSpy).toHaveBeenCalledWith('some query');
        });
      });
    });

    describe('archive redirection', () => {
      it('should redirect to `docs` if deployment mode is `archive` and not at a docs page', () => {
        createTestingModule('', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');

        createTestingModule('resources', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');

        createTestingModule('guide/aot-compiler', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial/toh-pt1', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('docs', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api/core/getPlatform', 'archive');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
      });

      it('should redirect to `docs` if deployment mode is `next` and not at a docs page', () => {
        createTestingModule('', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');

        createTestingModule('resources', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');

        createTestingModule('guide/aot-compiler', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial/toh-pt1', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('docs', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api/core/getPlatform', 'next');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
      });

      it('should not redirect to `docs` if deployment mode is `stable` and not at a docs page', () => {
        createTestingModule('', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('resources', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('guide/aot-compiler', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('tutorial/toh-pt1', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('docs', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();

        createTestingModule('api/core/getPlatform', 'stable');
        initializeTest();
        expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
      });
    });
  });

  describe('with mocked DocViewer', () => {
    const getDocViewer = () => fixture.debugElement.query(By.css('aio-doc-viewer'));
    const triggerDocRendered = () => getDocViewer().triggerEventHandler('docRendered', {});

    beforeEach(() => {
      createTestingModule('a/b');
      // Remove the DocViewer for this test and hide the missing component message
      TestBed.overrideModule(AppModule, {
        remove: { declarations: [DocViewerComponent] },
        add: { schemas: [NO_ERRORS_SCHEMA] }
      });
    });

    describe('initial rendering', () => {
      it('should initially add the starting class until the first document is rendered', fakeAsync(() => {
        const getSidenavContainer = () => fixture.debugElement.query(By.css('md-sidenav-container'));

        initializeTest();

        expect(component.isStarting).toBe(true);
        expect(getSidenavContainer().classes['starting']).toBe(true);

        triggerDocRendered();
        fixture.detectChanges();
        expect(component.isStarting).toBe(true);
        expect(getSidenavContainer().classes['starting']).toBe(true);

        tick(499);
        fixture.detectChanges();
        expect(component.isStarting).toBe(true);
        expect(getSidenavContainer().classes['starting']).toBe(true);

        tick(2);
        fixture.detectChanges();
        expect(component.isStarting).toBe(false);
        expect(getSidenavContainer().classes['starting']).toBe(false);
      }));
    });

    describe('progress bar', () => {
      const SHOW_DELAY = 200;
      const HIDE_DELAY = 500;
      const getProgressBar = () => fixture.debugElement.query(By.directive(MdProgressBar));
      const initializeAndCompleteNavigation = () => {
        initializeTest();
        triggerDocRendered();
        tick(HIDE_DELAY);
      };

      it('should initially be hidden', () => {
        initializeTest();
        expect(getProgressBar()).toBeFalsy();
      });

      it('should be shown (after a delay) when the path changes', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('c/d');

        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();

        tick(SHOW_DELAY - 1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();

        tick(1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeTruthy();
      }));

      it('should not be shown when the URL changes but the path remains the same', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('a/b');

        tick(SHOW_DELAY);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();
      }));

      it('should not be shown when re-navigating to the empty path', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('');
        triggerDocRendered();

        locationService.urlSubject.next('');

        tick(SHOW_DELAY);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();

        tick(HIDE_DELAY);   // Fire the remaining timer or `fakeAsync()` complains.
      }));

      it('should not be shown if the doc is rendered quickly', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('c/d');

        tick(SHOW_DELAY - 1);
        triggerDocRendered();

        tick(1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();

        tick(HIDE_DELAY);   // Fire the remaining timer or `fakeAsync()` complains.
      }));

      it('should be shown if rendering the doc takes too long', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('c/d');

        tick(SHOW_DELAY);
        triggerDocRendered();

        fixture.detectChanges();
        expect(getProgressBar()).toBeTruthy();

        tick(HIDE_DELAY);   // Fire the remaining timer or `fakeAsync()` complains.
      }));

      it('should be hidden (after a delay) once the doc is rendered', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('c/d');

        tick(SHOW_DELAY);
        triggerDocRendered();

        fixture.detectChanges();
        expect(getProgressBar()).toBeTruthy();

        tick(HIDE_DELAY - 1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeTruthy();

        tick(1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();
      }));

      it('should only take the latest request into account', fakeAsync(() => {
        initializeAndCompleteNavigation();
        locationService.urlSubject.next('c/d');   // The URL changes.
        locationService.urlSubject.next('e/f');   // The URL changes again before `onDocRendered()`.

        tick(SHOW_DELAY - 1);   // `onDocRendered()` is triggered (for the last doc),
        triggerDocRendered();   // before the progress bar is shown.

        tick(1);
        fixture.detectChanges();
        expect(getProgressBar()).toBeFalsy();

        tick(HIDE_DELAY);   // Fire the remaining timer or `fakeAsync()` complains.
      }));
    });

  });

});

//// test helpers ////

function createTestingModule(initialUrl: string, mode: string = 'stable') {
  const mockLocationService = new MockLocationService(initialUrl);
  TestBed.resetTestingModule();
  TestBed.configureTestingModule({
    imports: [ AppModule ],
    providers: [
      { provide: APP_BASE_HREF, useValue: '/' },
      { provide: GaService, useClass: TestGaService },
      { provide: HttpClient, useClass: TestHttpClient },
      { provide: LocationService, useFactory: () => mockLocationService },
      { provide: Logger, useClass: MockLogger },
      { provide: SearchService, useClass: MockSearchService },
      { provide: Deployment, useFactory: () => {
        const deployment = new Deployment(mockLocationService as any);
        deployment.mode = mode;
        return deployment;
      }},
    ]
  });
}

class TestGaService {
  locationChanged = jasmine.createSpy('locationChanged');
}

class TestHttpClient {

  static versionInfo = {
    raw: '4.0.0-rc.6',
    major: 4,
    minor: 0,
    patch: 0,
    prerelease: [ 'local' ],
    build: 'sha.73808dd',
    version: '4.0.0-local',
    codeName: 'snapshot',
    isSnapshot: true,
    full: '4.0.0-local+sha.73808dd',
    branch: 'master',
    commitSHA: '73808dd38b5ccd729404936834d1568bd066de81'
  };

  static docVersions: NavigationNode[] = [
    { title: 'v2', url: 'https://v2.angular.io' }
  ];

  // tslint:disable:quotemark
  navJson = {
    "TopBar": [
      {
        "url": "features",
        "title": "Features"
      },
      {
        "url": "no-title",
        "title": "No Title"
      },
    ],
    "SideNav": [
      {
      "title": "Core",
      "tooltip": "Learn the core capabilities of Angular",
      "children": [
          {
            "url": "guide/pipes",
            "title": "Pipes",
            "tooltip": "Pipes transform displayed values within a template."
          },
          {
            "url": "guide/bags",
            "title": "Bags",
            "tooltip": "Pack your bags for a code adventure."
          }
        ]
      },
      {
        "url": "api",
        "title": "API",
        "tooltip": "Details of the Angular classes and values."
      }
    ],
    "docVersions": TestHttpClient.docVersions,

    "__versionInfo": TestHttpClient.versionInfo,
  };

  get(url: string) {
    let data;
    if (/navigation\.json/.test(url)) {
      data = this.navJson;
    } else {
      const match = /generated\/docs\/(.+)\.json/.exec(url);
      const id = match[1];
      // Make up a title for test purposes
      const title = id.split('/').pop().replace(/^([a-z])/, (_, letter) => letter.toUpperCase());
      const h1 = (id === 'no-title') ? '' : `<h1>${title}</h1>`;
      const contents = `${h1}<h2 id="#somewhere">Some heading</h2>`;
      data = { id, contents };
    }
    return of(data);
  }
}
back to top