
This commit updates the version of Angular CLI used in angular.io to version 10.0.1. It also reverts some changes (namely commits 38dfbc775f1 and eee2fd22e0a) which were made due to an older bug that is fixed in the latest version. See #37688 for more details. Fixes #37699 PR Close #37898
736 lines
29 KiB
TypeScript
736 lines
29 KiB
TypeScript
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { Meta, Title } from '@angular/platform-browser';
|
|
|
|
import { Observable, asapScheduler, of } from 'rxjs';
|
|
|
|
import { FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
|
|
import { Logger } from 'app/shared/logger.service';
|
|
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
|
import { TocService } from 'app/shared/toc.service';
|
|
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
|
import {
|
|
MockTitle, MockTocService, ObservableWithSubscriptionSpies,
|
|
TestDocViewerComponent, TestModule, TestParentComponent, MockElementsLoader
|
|
} from 'testing/doc-viewer-utils';
|
|
import { MockLogger } from 'testing/logger.service';
|
|
import { DocViewerComponent, NO_ANIMATIONS } from './doc-viewer.component';
|
|
|
|
describe('DocViewerComponent', () => {
|
|
let parentFixture: ComponentFixture<TestParentComponent>;
|
|
let parentComponent: TestParentComponent;
|
|
let docViewerEl: HTMLElement;
|
|
let docViewer: TestDocViewerComponent;
|
|
|
|
const safeFlushAsapScheduler = () => asapScheduler.actions.length && asapScheduler.flush();
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [CustomElementsModule, TestModule],
|
|
});
|
|
|
|
parentFixture = TestBed.createComponent(TestParentComponent);
|
|
parentComponent = parentFixture.componentInstance;
|
|
|
|
parentFixture.detectChanges();
|
|
|
|
docViewerEl = parentFixture.debugElement.children[0].nativeElement;
|
|
docViewer = parentComponent.docViewer as any;
|
|
});
|
|
|
|
it('should create a `DocViewer`', () => {
|
|
expect(docViewer).toEqual(jasmine.any(DocViewerComponent));
|
|
});
|
|
|
|
describe('#doc', () => {
|
|
let renderSpy: jasmine.Spy;
|
|
|
|
const setCurrentDoc = (newDoc: TestParentComponent['currentDoc']) => {
|
|
parentComponent.currentDoc = newDoc;
|
|
parentFixture.detectChanges(); // Run change detection to propagate the new doc to `DocViewer`.
|
|
safeFlushAsapScheduler(); // Flush `asapScheduler` to trigger `DocViewer#render()`.
|
|
};
|
|
|
|
beforeEach(() => renderSpy = spyOn(docViewer, 'render').and.callFake(() => of(undefined)));
|
|
|
|
it('should render the new document', () => {
|
|
setCurrentDoc({contents: 'foo', id: 'bar'});
|
|
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
expect(renderSpy.calls.mostRecent().args).toEqual([{id: 'bar', contents: 'foo'}]);
|
|
|
|
setCurrentDoc({contents: null, id: 'baz'});
|
|
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
expect(renderSpy.calls.mostRecent().args).toEqual([{id: 'baz', contents: null}]);
|
|
});
|
|
|
|
it('should unsubscribe from the previous "render" observable upon new document', () => {
|
|
const obs = new ObservableWithSubscriptionSpies();
|
|
renderSpy.and.returnValue(obs);
|
|
|
|
setCurrentDoc({contents: 'foo', id: 'bar'});
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
|
expect(obs.unsubscribeSpies[0]).not.toHaveBeenCalled();
|
|
|
|
setCurrentDoc({contents: 'baz', id: 'qux'});
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(2);
|
|
expect(obs.unsubscribeSpies[0]).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should ignore falsy document values', () => {
|
|
setCurrentDoc(null);
|
|
expect(renderSpy).not.toHaveBeenCalled();
|
|
|
|
setCurrentDoc(undefined);
|
|
expect(renderSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('#ngOnDestroy()', () => {
|
|
it('should stop responding to document changes', () => {
|
|
const renderSpy = spyOn(docViewer, 'render').and.callFake(() => of(undefined));
|
|
|
|
expect(renderSpy).not.toHaveBeenCalled();
|
|
|
|
docViewer.doc = {contents: 'Some content', id: 'some-id'};
|
|
safeFlushAsapScheduler();
|
|
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
|
|
docViewer.ngOnDestroy();
|
|
|
|
docViewer.doc = {contents: 'Other content', id: 'other-id'};
|
|
safeFlushAsapScheduler();
|
|
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
|
|
docViewer.doc = {contents: 'More content', id: 'more-id'};
|
|
safeFlushAsapScheduler();
|
|
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('#prepareTitleAndToc()', () => {
|
|
const EMPTY_DOC = '';
|
|
const DOC_WITHOUT_H1 = 'Some content';
|
|
const DOC_WITH_H1 = '<h1>Features</h1>Some content';
|
|
const DOC_WITH_NO_TOC_H1 = '<h1 class="no-toc">Features</h1>Some content';
|
|
const DOC_WITH_EMBEDDED_TOC = '<h1>Features</h1><aio-toc class="embedded"></aio-toc>Some content';
|
|
const DOC_WITH_EMBEDDED_TOC_WITHOUT_H1 = '<aio-toc class="embedded"></aio-toc>Some content';
|
|
const DOC_WITH_EMBEDDED_TOC_WITH_NO_TOC_H1 = '<aio-toc class="embedded"></aio-toc>Some content';
|
|
const DOC_WITH_HIDDEN_H1_CONTENT = '<h1><i style="visibility: hidden">link</i>Features</h1>Some content';
|
|
let titleService: MockTitle;
|
|
let tocService: MockTocService;
|
|
let targetEl: HTMLElement;
|
|
|
|
const getTocEl = () => targetEl.querySelector('aio-toc');
|
|
const doPrepareTitleAndToc = (contents: string, docId = '') => {
|
|
targetEl.innerHTML = contents;
|
|
return docViewer.prepareTitleAndToc(targetEl, docId);
|
|
};
|
|
const doAddTitleAndToc = (contents: string, docId = '') => {
|
|
const addTitleAndToc = doPrepareTitleAndToc(contents, docId);
|
|
return addTitleAndToc();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
titleService = TestBed.inject(Title) as unknown as MockTitle;
|
|
tocService = TestBed.inject(TocService) as unknown as MockTocService;
|
|
|
|
targetEl = document.createElement('div');
|
|
document.body.appendChild(targetEl); // Required for `innerText` to work as expected.
|
|
});
|
|
|
|
afterEach(() => document.body.removeChild(targetEl));
|
|
|
|
it('should return a function for doing the actual work', () => {
|
|
const addTitleAndToc = doPrepareTitleAndToc(DOC_WITH_H1);
|
|
|
|
expect(getTocEl()).toBeTruthy();
|
|
expect(titleService.setTitle).not.toHaveBeenCalled();
|
|
expect(tocService.reset).not.toHaveBeenCalled();
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
|
|
addTitleAndToc();
|
|
|
|
expect(titleService.setTitle).toHaveBeenCalledTimes(1);
|
|
expect(tocService.reset).toHaveBeenCalledTimes(1);
|
|
expect(tocService.genToc).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe('(title)', () => {
|
|
it('should set the title if there is an `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITH_H1);
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Features');
|
|
});
|
|
|
|
it('should set the title if there is a `.no-toc` `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITH_NO_TOC_H1);
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Features');
|
|
});
|
|
|
|
it('should set the default title if there is no `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITHOUT_H1);
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
|
|
|
|
doAddTitleAndToc(EMPTY_DOC);
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
|
|
});
|
|
|
|
it('should not include hidden content of the `<h1>` heading in the title', () => {
|
|
doAddTitleAndToc(DOC_WITH_HIDDEN_H1_CONTENT);
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Features');
|
|
});
|
|
|
|
it('should fall back to `textContent` if `innerText` is not available', () => {
|
|
const querySelector = targetEl.querySelector;
|
|
spyOn(targetEl, 'querySelector').and.callFake((selector: string) => {
|
|
const elem = querySelector.call(targetEl, selector);
|
|
return elem && Object.defineProperties(elem, {
|
|
innerText: {value: undefined},
|
|
textContent: {value: 'Text Content'},
|
|
});
|
|
});
|
|
|
|
doAddTitleAndToc(DOC_WITH_HIDDEN_H1_CONTENT);
|
|
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Text Content');
|
|
});
|
|
|
|
it('should still use `innerText` if available but empty', () => {
|
|
const querySelector = targetEl.querySelector;
|
|
spyOn(targetEl, 'querySelector').and.callFake((selector: string) => {
|
|
const elem = querySelector.call(targetEl, selector);
|
|
return elem && Object.defineProperties(elem, {
|
|
innerText: { value: '' },
|
|
textContent: { value: 'Text Content' }
|
|
});
|
|
});
|
|
|
|
doAddTitleAndToc(DOC_WITH_HIDDEN_H1_CONTENT);
|
|
|
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
|
|
});
|
|
});
|
|
|
|
describe('(ToC)', () => {
|
|
describe('needed', () => {
|
|
it('should add an embedded ToC element if there is an `<h1>` heading', () => {
|
|
doPrepareTitleAndToc(DOC_WITH_H1);
|
|
const tocEl = getTocEl()!;
|
|
|
|
expect(tocEl).toBeTruthy();
|
|
expect(tocEl.classList.contains('embedded')).toBe(true);
|
|
});
|
|
|
|
it('should not add a second ToC element if there a hard coded one in place', () => {
|
|
doPrepareTitleAndToc(DOC_WITH_EMBEDDED_TOC);
|
|
expect(targetEl.querySelectorAll('aio-toc').length).toEqual(1);
|
|
});
|
|
});
|
|
|
|
|
|
describe('not needed', () => {
|
|
it('should not add a ToC element if there is a `.no-toc` `<h1>` heading', () => {
|
|
doPrepareTitleAndToc(DOC_WITH_NO_TOC_H1);
|
|
expect(getTocEl()).toBeFalsy();
|
|
});
|
|
|
|
it('should not add a ToC element if there is no `<h1>` heading', () => {
|
|
doPrepareTitleAndToc(DOC_WITHOUT_H1);
|
|
expect(getTocEl()).toBeFalsy();
|
|
|
|
doPrepareTitleAndToc(EMPTY_DOC);
|
|
expect(getTocEl()).toBeFalsy();
|
|
});
|
|
|
|
it('should remove ToC a hard coded one', () => {
|
|
doPrepareTitleAndToc(DOC_WITH_EMBEDDED_TOC_WITHOUT_H1);
|
|
expect(getTocEl()).toBeFalsy();
|
|
|
|
doPrepareTitleAndToc(DOC_WITH_EMBEDDED_TOC_WITH_NO_TOC_H1);
|
|
expect(getTocEl()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
|
|
it('should generate ToC entries if there is an `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITH_H1, 'foo');
|
|
|
|
expect(tocService.genToc).toHaveBeenCalledTimes(1);
|
|
expect(tocService.genToc).toHaveBeenCalledWith(targetEl, 'foo');
|
|
});
|
|
|
|
it('should not generate ToC entries if there is a `.no-toc` `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITH_NO_TOC_H1);
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not generate ToC entries if there is no `<h1>` heading', () => {
|
|
doAddTitleAndToc(DOC_WITHOUT_H1);
|
|
doAddTitleAndToc(EMPTY_DOC);
|
|
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should always reset the ToC (before generating the new one)', () => {
|
|
doAddTitleAndToc(DOC_WITH_H1, 'foo');
|
|
expect(tocService.reset).toHaveBeenCalledTimes(1);
|
|
expect(tocService.reset).toHaveBeenCalledBefore(tocService.genToc);
|
|
expect(tocService.genToc).toHaveBeenCalledWith(targetEl, 'foo');
|
|
|
|
tocService.genToc.calls.reset();
|
|
|
|
doAddTitleAndToc(DOC_WITH_NO_TOC_H1, 'bar');
|
|
expect(tocService.reset).toHaveBeenCalledTimes(2);
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
|
|
doAddTitleAndToc(DOC_WITHOUT_H1, 'baz');
|
|
expect(tocService.reset).toHaveBeenCalledTimes(3);
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
|
|
doAddTitleAndToc(EMPTY_DOC, 'qux');
|
|
expect(tocService.reset).toHaveBeenCalledTimes(4);
|
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#render()', () => {
|
|
let prepareTitleAndTocSpy: jasmine.Spy;
|
|
let swapViewsSpy: jasmine.Spy;
|
|
let loadElementsSpy: jasmine.Spy;
|
|
|
|
const doRender = (contents: string | null, id = 'foo') =>
|
|
docViewer.render({contents, id}).toPromise();
|
|
|
|
beforeEach(() => {
|
|
const elementsLoader = TestBed.inject(ElementsLoader) as Partial<ElementsLoader> as MockElementsLoader;
|
|
loadElementsSpy = elementsLoader.loadContainedCustomElements.and.callFake(() => of(undefined));
|
|
prepareTitleAndTocSpy = spyOn(docViewer, 'prepareTitleAndToc');
|
|
swapViewsSpy = spyOn(docViewer, 'swapViews').and.callFake(() => of(undefined));
|
|
});
|
|
|
|
it('should return an `Observable`', () => {
|
|
expect(docViewer.render({contents: '', id: ''})).toEqual(jasmine.any(Observable));
|
|
});
|
|
|
|
describe('(contents, title, ToC)', () => {
|
|
beforeEach(() => swapViewsSpy.and.callThrough());
|
|
|
|
it('should display the document contents', async () => {
|
|
const contents = '<h1>Hello,</h1> <div>world!</div>';
|
|
await doRender(contents);
|
|
|
|
expect(docViewerEl.innerHTML).toContain(contents);
|
|
expect(docViewerEl.textContent).toBe('Hello, world!');
|
|
});
|
|
|
|
it('should display nothing if the document has no contents', async () => {
|
|
await doRender('Test');
|
|
expect(docViewerEl.textContent).toBe('Test');
|
|
|
|
await doRender('');
|
|
expect(docViewerEl.textContent).toBe('');
|
|
|
|
docViewer.currViewContainer.innerHTML = 'Test';
|
|
expect(docViewerEl.textContent).toBe('Test');
|
|
|
|
await doRender(null);
|
|
expect(docViewerEl.textContent).toBe('');
|
|
});
|
|
|
|
it('should prepare the title and ToC (before embedding components)', async () => {
|
|
prepareTitleAndTocSpy.and.callFake((targetEl: HTMLElement, docId: string) => {
|
|
expect(targetEl.innerHTML).toBe('Some content');
|
|
expect(docId).toBe('foo');
|
|
});
|
|
|
|
await doRender('Some content', 'foo');
|
|
|
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledBefore(loadElementsSpy);
|
|
});
|
|
|
|
it('should set the title and ToC (after the content has been set)', async () => {
|
|
const addTitleAndTocSpy = jasmine.createSpy('addTitleAndToc');
|
|
prepareTitleAndTocSpy.and.returnValue(addTitleAndTocSpy);
|
|
|
|
addTitleAndTocSpy.and.callFake(() => expect(docViewerEl.textContent).toBe('Foo content'));
|
|
await doRender('Foo content');
|
|
expect(addTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
|
|
addTitleAndTocSpy.and.callFake(() => expect(docViewerEl.textContent).toBe('Bar content'));
|
|
await doRender('Bar content');
|
|
expect(addTitleAndTocSpy).toHaveBeenCalledTimes(2);
|
|
|
|
addTitleAndTocSpy.and.callFake(() => expect(docViewerEl.textContent).toBe(''));
|
|
await doRender('');
|
|
expect(addTitleAndTocSpy).toHaveBeenCalledTimes(3);
|
|
|
|
addTitleAndTocSpy.and.callFake(() => expect(docViewerEl.textContent).toBe('Qux content'));
|
|
await doRender('Qux content');
|
|
expect(addTitleAndTocSpy).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it('should remove the "noindex" meta tag if the document is valid', async () => {
|
|
await doRender('foo', 'bar');
|
|
expect(TestBed.inject(Meta).removeTag).toHaveBeenCalledWith('name="robots"');
|
|
});
|
|
|
|
it('should add the "noindex" meta tag if the document is 404', async () => {
|
|
await doRender('missing', FILE_NOT_FOUND_ID);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
|
|
it('should add a "noindex" meta tag if the document fetching fails', async () => {
|
|
await doRender('error', FETCHING_ERROR_ID);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
});
|
|
|
|
describe('(embedding components)', () => {
|
|
it('should embed components', async () => {
|
|
await doRender('Some content');
|
|
expect(loadElementsSpy).toHaveBeenCalledTimes(1);
|
|
expect(loadElementsSpy).toHaveBeenCalledWith(docViewer.nextViewContainer);
|
|
});
|
|
|
|
it('should attempt to embed components even if the document is empty', async () => {
|
|
await doRender('');
|
|
await doRender(null);
|
|
|
|
expect(loadElementsSpy).toHaveBeenCalledTimes(2);
|
|
expect(loadElementsSpy.calls.argsFor(0)).toEqual([docViewer.nextViewContainer]);
|
|
expect(loadElementsSpy.calls.argsFor(1)).toEqual([docViewer.nextViewContainer]);
|
|
});
|
|
|
|
it('should unsubscribe from the previous "embed" observable when unsubscribed from', () => {
|
|
const obs = new ObservableWithSubscriptionSpies();
|
|
loadElementsSpy.and.returnValue(obs);
|
|
|
|
const renderObservable = docViewer.render({contents: 'Some content', id: 'foo'});
|
|
const subscription = renderObservable.subscribe();
|
|
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
|
expect(obs.unsubscribeSpies[0]).not.toHaveBeenCalled();
|
|
|
|
subscription.unsubscribe();
|
|
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
|
expect(obs.unsubscribeSpies[0]).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('(swapping views)', () => {
|
|
it('should still swap the views if the document is empty', async () => {
|
|
await doRender('');
|
|
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
|
|
|
await doRender(null);
|
|
expect(swapViewsSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should pass the `addTitleAndToc` callback', async () => {
|
|
const addTitleAndTocSpy = jasmine.createSpy('addTitleAndToc');
|
|
prepareTitleAndTocSpy.and.returnValue(addTitleAndTocSpy);
|
|
|
|
await doRender('<div></div>');
|
|
|
|
expect(swapViewsSpy).toHaveBeenCalledWith(addTitleAndTocSpy);
|
|
});
|
|
|
|
it('should unsubscribe from the previous "swap" observable when unsubscribed from', () => {
|
|
const obs = new ObservableWithSubscriptionSpies();
|
|
swapViewsSpy.and.returnValue(obs);
|
|
|
|
const renderObservable = docViewer.render({contents: 'Hello, world!', id: 'foo'});
|
|
const subscription = renderObservable.subscribe();
|
|
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
|
expect(obs.unsubscribeSpies[0]).not.toHaveBeenCalled();
|
|
|
|
subscription.unsubscribe();
|
|
|
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
|
expect(obs.unsubscribeSpies[0]).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('(on error) should clean up, log the error and recover', () => {
|
|
let logger: MockLogger;
|
|
|
|
beforeEach(() => {
|
|
logger = TestBed.inject(Logger) as unknown as MockLogger;
|
|
});
|
|
|
|
it('when `prepareTitleAndTocSpy()` fails', async () => {
|
|
const error = Error('Typical `prepareTitleAndToc()` error');
|
|
prepareTitleAndTocSpy.and.callFake(() => {
|
|
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
throw error;
|
|
});
|
|
|
|
await doRender('Some content', 'foo');
|
|
|
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
expect(swapViewsSpy).not.toHaveBeenCalled();
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
expect(logger.output.error).toEqual([
|
|
[jasmine.any(Error)]
|
|
]);
|
|
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'foo': ${error.stack}`);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
|
|
it('when `EmbedComponentsService.embedInto()` fails', async () => {
|
|
const error = Error('Typical `embedInto()` error');
|
|
loadElementsSpy.and.callFake(() => {
|
|
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
throw error;
|
|
});
|
|
|
|
await doRender('Some content', 'bar');
|
|
|
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
expect(loadElementsSpy).toHaveBeenCalledTimes(1);
|
|
expect(swapViewsSpy).not.toHaveBeenCalled();
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
expect(logger.output.error).toEqual([
|
|
[jasmine.any(Error)]
|
|
]);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
|
|
it('when `swapViews()` fails', async () => {
|
|
const error = Error('Typical `swapViews()` error');
|
|
swapViewsSpy.and.callFake(() => {
|
|
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
throw error;
|
|
});
|
|
|
|
await doRender('Some content', 'qux');
|
|
|
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
expect(logger.output.error).toEqual([
|
|
[jasmine.any(Error)]
|
|
]);
|
|
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error.stack}`);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
|
|
it('when something fails with non-Error', async () => {
|
|
const error = 'Typical string error';
|
|
swapViewsSpy.and.callFake(() => {
|
|
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
throw error;
|
|
});
|
|
|
|
await doRender('Some content', 'qux');
|
|
|
|
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
expect(logger.output.error).toEqual([
|
|
[jasmine.any(Error)]
|
|
]);
|
|
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error}`);
|
|
expect(TestBed.inject(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
|
});
|
|
});
|
|
|
|
describe('(events)', () => {
|
|
it('should emit `docReady` after loading elements', async () => {
|
|
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
|
docViewer.docReady.subscribe(onDocReadySpy);
|
|
|
|
await doRender('Some content');
|
|
|
|
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
|
expect(loadElementsSpy).toHaveBeenCalledBefore(onDocReadySpy);
|
|
});
|
|
|
|
it('should emit `docReady` before swapping views', async () => {
|
|
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
|
docViewer.docReady.subscribe(onDocReadySpy);
|
|
|
|
await doRender('Some content');
|
|
|
|
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
|
expect(onDocReadySpy).toHaveBeenCalledBefore(swapViewsSpy);
|
|
});
|
|
|
|
it('should emit `docRendered` after swapping views', async () => {
|
|
const onDocRenderedSpy = jasmine.createSpy('onDocRendered');
|
|
docViewer.docRendered.subscribe(onDocRenderedSpy);
|
|
|
|
await doRender('Some content');
|
|
|
|
expect(onDocRenderedSpy).toHaveBeenCalledTimes(1);
|
|
expect(swapViewsSpy).toHaveBeenCalledBefore(onDocRenderedSpy);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#swapViews()', () => {
|
|
let oldCurrViewContainer: HTMLElement;
|
|
let oldNextViewContainer: HTMLElement;
|
|
|
|
const doSwapViews = (cb?: () => void) => docViewer.swapViews(cb).toPromise();
|
|
|
|
beforeEach(() => {
|
|
oldCurrViewContainer = docViewer.currViewContainer;
|
|
oldNextViewContainer = docViewer.nextViewContainer;
|
|
|
|
oldCurrViewContainer.innerHTML = 'Current view';
|
|
oldNextViewContainer.innerHTML = 'Next view';
|
|
|
|
docViewerEl.appendChild(oldCurrViewContainer);
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
});
|
|
|
|
[true, false].forEach(animationsEnabled => {
|
|
describe(`(animationsEnabled: ${animationsEnabled})`, () => {
|
|
beforeEach(() => DocViewerComponent.animationsEnabled = animationsEnabled);
|
|
afterEach(() => DocViewerComponent.animationsEnabled = true);
|
|
|
|
[true, false].forEach(noAnimations => {
|
|
describe(`(.${NO_ANIMATIONS}: ${noAnimations})`, () => {
|
|
beforeEach(() => docViewerEl.classList[noAnimations ? 'add' : 'remove'](NO_ANIMATIONS));
|
|
|
|
it('should return an observable', done => {
|
|
docViewer.swapViews().subscribe(done, done.fail);
|
|
});
|
|
|
|
it('should swap the views', async () => {
|
|
await doSwapViews();
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
|
|
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
|
|
|
|
await doSwapViews();
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
expect(docViewer.currViewContainer).toBe(oldCurrViewContainer);
|
|
expect(docViewer.nextViewContainer).toBe(oldNextViewContainer);
|
|
});
|
|
|
|
it('should emit `docRemoved` after removing the leaving view', async () => {
|
|
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved').and.callFake(() => {
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
});
|
|
|
|
docViewer.docRemoved.subscribe(onDocRemovedSpy);
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
|
|
await doSwapViews();
|
|
|
|
expect(onDocRemovedSpy).toHaveBeenCalledTimes(1);
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
});
|
|
|
|
it('should not emit `docRemoved` if the leaving view is already removed', async () => {
|
|
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved');
|
|
|
|
docViewer.docRemoved.subscribe(onDocRemovedSpy);
|
|
docViewerEl.removeChild(oldCurrViewContainer);
|
|
|
|
await doSwapViews();
|
|
|
|
expect(onDocRemovedSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit `docInserted` after inserting the entering view', async () => {
|
|
const onDocInsertedSpy = jasmine.createSpy('onDocInserted').and.callFake(() => {
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
});
|
|
|
|
docViewer.docInserted.subscribe(onDocInsertedSpy);
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
|
|
await doSwapViews();
|
|
|
|
expect(onDocInsertedSpy).toHaveBeenCalledTimes(1);
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
});
|
|
|
|
it('should call the callback after inserting the entering view', async () => {
|
|
const onInsertedCb = jasmine.createSpy('onInsertedCb').and.callFake(() => {
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
});
|
|
const onDocInsertedSpy = jasmine.createSpy('onDocInserted');
|
|
|
|
docViewer.docInserted.subscribe(onDocInsertedSpy);
|
|
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
|
|
|
await doSwapViews(onInsertedCb);
|
|
|
|
expect(onInsertedCb).toHaveBeenCalledTimes(1);
|
|
expect(onInsertedCb).toHaveBeenCalledBefore(onDocInsertedSpy);
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
});
|
|
|
|
it('should empty the previous view', async () => {
|
|
await doSwapViews();
|
|
|
|
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
|
|
docViewer.nextViewContainer.innerHTML = 'Next view 2';
|
|
await doSwapViews();
|
|
|
|
expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2');
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
});
|
|
|
|
if (animationsEnabled && !noAnimations) {
|
|
// Only test this when there are animations. Without animations, the views are swapped
|
|
// synchronously, so there is no need (or way) to abort.
|
|
it('should abort swapping if the returned observable is unsubscribed from', async () => {
|
|
docViewer.swapViews().subscribe().unsubscribe();
|
|
await doSwapViews();
|
|
|
|
// Since the first call was cancelled, only one swapping should have taken place.
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
|
|
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
|
|
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
});
|
|
} else {
|
|
it('should swap views synchronously when animations are disabled', () => {
|
|
const cbSpy = jasmine.createSpy('cb');
|
|
|
|
docViewer.swapViews(cbSpy).subscribe();
|
|
|
|
expect(cbSpy).toHaveBeenCalledTimes(1);
|
|
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
|
|
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
|
|
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
|
|
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
|
|
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
|
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|