import { ReflectiveInjector, SecurityContext } from '@angular/core';
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TocItem, TocService } from './toc.service';
describe('TocService', () => {
let injector: ReflectiveInjector;
let tocService: TocService;
let lastTocList: TocItem[];
// call TocService.genToc
function callGenToc(html = '', docId = 'fizz/buzz'): HTMLDivElement {
const el = document.createElement('div');
el.innerHTML = html;
tocService.genToc(el, docId);
return el;
}
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
{ provide: DomSanitizer, useClass: TestDomSanitizer },
{ provide: DOCUMENT, useValue: document },
TocService,
]);
tocService = injector.get(TocService);
tocService.tocList.subscribe(tocList => lastTocList = tocList);
});
it('should be creatable', () => {
expect(tocService).toBeTruthy();
});
describe('tocList', () => {
it('should emit the latest value to new subscribers', () => {
let value1: TocItem[];
let value2: TocItem[];
tocService.tocList.next([] as TocItem[]);
tocService.tocList.subscribe(v => value1 = v);
expect(value1).toEqual([]);
tocService.tocList.next([{}, {}] as TocItem[]);
tocService.tocList.subscribe(v => value2 = v);
expect(value2).toEqual([{}, {}]);
});
it('should emit the same values to all subscribers', () => {
const emittedValues: TocItem[][] = [];
tocService.tocList.subscribe(v => emittedValues.push(v));
tocService.tocList.subscribe(v => emittedValues.push(v));
tocService.tocList.next([{ title: 'A' }, { title: 'B' }] as TocItem[]);
expect(emittedValues).toEqual([
[{ title: 'A' }, { title: 'B' }],
[{ title: 'A' }, { title: 'B' }]
]);
});
});
describe('should clear tocList', () => {
beforeEach(() => {
// Start w/ dummy data from previous usage
tocService.tocList.next([{}, {}] as TocItem[]);
expect(lastTocList).not.toEqual([]);
});
it('when reset()', () => {
tocService.reset();
expect(lastTocList).toEqual([]);
});
it('when given undefined doc element', () => {
tocService.genToc(undefined);
expect(lastTocList).toEqual([]);
});
it('when given doc element w/ no headings', () => {
callGenToc('
This
and
that
');
expect(lastTocList).toEqual([]);
});
it('when given doc element w/ headings other than h2 & h3', () => {
callGenToc('This
and
that
');
expect(lastTocList).toEqual([]);
});
it('when given doc element w/ no-toc headings', () => {
// tolerates different spellings/casing of the no-toc class
callGenToc(`
one
some one
two
some two
three
some three
four
some four
`);
expect(lastTocList).toEqual([]);
});
});
describe('when given many headings', () => {
let docId: string;
let docEl: HTMLDivElement;
let headings: NodeListOf;
beforeEach(() => {
docId = 'fizz/buzz';
docEl = callGenToc(`
Fun with TOC
Heading one
h2 toc 0
H2 Two
h2 toc 1
H2 Three
h2 toc 2
H3 3a
h3 toc 3
H3 3b
h3 toc 4
H4 of h3-3b
an h4
H2 4 repeat
h2 toc 5
H2 4 repeat
h2 toc 6
Skippy
Skip this header
H2 6
h2 toc 7
H3 6a
h3 toc 8
`, docId);
headings = docEl.querySelectorAll('h1,h2,h3,h4') as NodeListOf;
});
it('should have tocList with expect number of TocItems', () => {
// should ignore h1, h4, and the no-toc h2
expect(lastTocList.length).toEqual(headings.length - 3);
});
it('should have href with docId and heading\'s id', () => {
const tocItem = lastTocList[0];
expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`);
});
it('should have level "h2" for an ', () => {
const tocItem = lastTocList[0];
expect(tocItem.level).toEqual('h2');
});
it('should have level "h3" for an ', () => {
const tocItem = lastTocList[3];
expect(tocItem.level).toEqual('h3');
});
it('should have title which is heading\'s innerText ', () => {
const heading = headings[3];
const tocItem = lastTocList[2];
expect(heading.innerText).toEqual(tocItem.title);
});
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
const heading = headings[3];
const content = lastTocList[2].content;
expect((content).changingThisBreaksApplicationSecurity)
.toEqual(heading.innerHTML);
});
it('should calculate and set id of heading without an id', () => {
const id = headings[2].getAttribute('id');
expect(id).toEqual('h2-two');
});
it('should have href with docId and calculated heading id', () => {
const tocItem = lastTocList[1];
expect(tocItem.href).toEqual(`${docId}#h2-two`);
});
it('should ignore HTML in heading when calculating id', () => {
const id = headings[3].getAttribute('id');
const tocItem = lastTocList[2];
expect(id).toEqual('h2-three', 'heading id');
expect(tocItem.href).toEqual(`${docId}#h2-three`, 'tocItem href');
});
it('should avoid repeating an id when calculating', () => {
const tocItem4a = lastTocList[5];
const tocItem4b = lastTocList[6];
expect(tocItem4a.href).toEqual(`${docId}#h2-4-repeat`, 'first');
expect(tocItem4b.href).toEqual(`${docId}#h2-4-repeat-2`, 'second');
});
});
describe('TocItem for an h2 with anchor link and extra whitespace', () => {
let docId: string;
let docEl: HTMLDivElement;
let tocItem: TocItem;
let expectedTocContent: string;
beforeEach(() => {
docId = 'fizz/buzz/';
expectedTocContent = 'Setup to develop locally.';
// An almost-actual ... with extra whitespace
docEl = callGenToc(`
${expectedTocContent}
`, docId);
tocItem = lastTocList[0];
});
it('should have expected href', () => {
expect(tocItem.href).toEqual(`${docId}#setup-to-develop-locally`);
});
it('should have expected title', () => {
expect(tocItem.title).toEqual('Setup to develop locally.');
});
it('should have removed anchor link from tocItem html content', () => {
expect((tocItem.content)
.changingThisBreaksApplicationSecurity)
.toEqual('Setup to develop locally.');
});
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
expect(domSanitizer.bypassSecurityTrustHtml)
.toHaveBeenCalledWith(expectedTocContent);
});
});
});
interface TestSafeHtml extends SafeHtml {
changingThisBreaksApplicationSecurity: string;
getTypeName: () => string;
}
class TestDomSanitizer {
bypassSecurityTrustHtml = jasmine.createSpy('bypassSecurityTrustHtml')
.and.callFake(html => {
return {
changingThisBreaksApplicationSecurity: html,
getTypeName: () => 'HTML',
} as TestSafeHtml;
});
}