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; // 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); }); it('should be creatable', () => { expect(tocService).toBeTruthy(); }); describe('should clear tocList', () => { // Start w/ dummy data from previous usage beforeEach(() => tocService.tocList = [{}, {}] as TocItem[]); it('when reset()', () => { tocService.reset(); expect(tocService.tocList.length).toEqual(0); }); it('when given undefined doc element', () => { tocService.genToc(undefined); expect(tocService.tocList.length).toEqual(0); }); it('when given doc element w/ no headings', () => { callGenToc('

This

and

that

'); expect(tocService.tocList.length).toEqual(0); }); it('when given doc element w/ headings other than h2 & h3', () => { callGenToc('

This

and

that
'); expect(tocService.tocList.length).toEqual(0); }); 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(tocService.tocList.length).toEqual(0); }); }); describe('when given many headings', () => { let docId: string; let docEl: HTMLDivElement; let tocList: TocItem[]; 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); tocList = tocService.tocList; 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(tocList.length).toEqual(headings.length - 3); }); it('should have href with docId and heading\'s id', () => { const tocItem = tocList[0]; expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`); }); it('should have level "h2" for an

', () => { const tocItem = tocList[0]; expect(tocItem.level).toEqual('h2'); }); it('should have level "h3" for an

', () => { const tocItem = tocList[3]; expect(tocItem.level).toEqual('h3'); }); it('should have title which is heading\'s innerText ', () => { const heading = headings[3]; const tocItem = tocList[2]; expect(heading.innerText).toEqual(tocItem.title); }); it('should have "SafeHtml" content which is heading\'s innerHTML ', () => { const heading = headings[3]; const content = tocList[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 = tocList[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 = tocList[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 = tocList[5]; const tocItem4b = tocList[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 = tocService.tocList[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; }); }