/*! * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {provideRouter} from '@angular/router'; import {ExampleViewerContentLoader} from '../../../interfaces'; import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers'; import {ExampleViewer} from '../example-viewer/example-viewer.component'; import {DocViewer} from './docs-viewer.component'; import {IconComponent} from '../../icon/icon.component'; import {Breadcrumb} from '../../breadcrumb/breadcrumb.component'; import {NavigationState} from '../../../services'; import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component'; import {CopyLinkButton} from '../../copy-link-anchor/copy-link-anchor.component'; import {TableOfContents} from '../../table-of-contents/table-of-contents.component'; import {Clipboard} from '@angular/cdk/clipboard'; describe('DocViewer', () => { let exampleContentSpy: jasmine.SpyObj; let navigationStateSpy: jasmine.SpyObj; const exampleDocContentWithExampleViewerPlaceholders = `
A styled code example
      
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectorRef, Component, inject, signal} from '@angular/core';
import {Component, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'hello-world',
imports: [CommonModule],
templateUrl: './hello-world.html',
styleUrls: ['./hello-world.css'],
})
export default class HelloWorldComponent {
world = 'World';
world = 'World!!!';
count = signal(0);
changeDetector = inject(ChangeDetectorRef);
increase(): void {
this.count.update((previous) => {
return previous + 1;
});
this.changeDetector.detectChanges();
}
}
`; const exampleDocContentWithExpandedExampleViewerPlaceholders = `
      
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectorRef, Component, inject, signal} from '@angular/core';
import {Component, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'hello-world',
imports: [CommonModule],
templateUrl: './hello-world.html',
styleUrls: ['./hello-world.css'],
})
export default class HelloWorldComponent {
world = 'World';
world = 'World!!!';
count = signal(0);
changeDetector = inject(ChangeDetectorRef);
increase(): void {
this.count.update((previous) => {
return previous + 1;
});
this.changeDetector.detectChanges();
}
}
      
<h2>Hello {{ world }}</h2>
<button (click)="increase()">Increase</button>
<p>Counter: {{ count() }}</p>
`; const exampleContentWithIcons = `

Content

light_mode

More content

dark_mode `; const exampleContentWithBreadcrumbPlaceholder = `

Content

`; const exampleContentWithCodeSnippet = `
`; const exampleContentWithHeadings = `

Heading h2

Heading h3

`; const exampleContentWithDocsAnchor = `

Test Section

`; beforeEach(() => { exampleContentSpy = jasmine.createSpyObj('ExampleViewerContentLoader', ['getCodeExampleData']); navigationStateSpy = jasmine.createSpyObj(NavigationState, ['activeNavigationItem']); }); beforeEach(() => { TestBed.configureTestingModule({ imports: [DocViewer], providers: [ provideRouter([]), {provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy}, {provide: NavigationState, useValue: navigationStateSpy}, ], }); }); it('should load doc into innerHTML', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', 'hello world'); await fixture.whenStable(); expect(fixture.nativeElement.innerHTML).toBe('hello world'); }); it('should instantiate example viewer with only a single file', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', exampleDocContentWithExampleViewerPlaceholders); await fixture.whenStable(); const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer)); expect(exampleViewer).not.toBeNull(); const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton)); expect(copySourceCodeButton).not.toBeNull(); const checkIcon = copySourceCodeButton.query(By.directive(IconComponent)); expect((checkIcon.nativeElement as HTMLElement).classList).toContain( `material-symbols-outlined`, ); expect((checkIcon.nativeElement as HTMLElement).classList).toContain(`docs-check`); expect(checkIcon.nativeElement.innerHTML).toBe('check'); }); it('should display example viewer in multi file mode when provided example is multi file snippet', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput( 'docContent', exampleDocContentWithExpandedExampleViewerPlaceholders, ); await fixture.whenStable(); const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer)); expect(exampleViewer).not.toBeNull(); expect(exampleViewer.componentInstance.tabs().length).toBe(2); }); it('should render Icon component when content has element', async () => { const fixture = TestBed.createComponent(DocViewer); const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any); fixture.componentRef.setInput('docContent', exampleContentWithIcons); await fixture.whenStable(); expect(renderComponentSpy).toHaveBeenCalledTimes(2); expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(IconComponent); expect((renderComponentSpy.calls.allArgs()[0][1] as HTMLElement).innerText).toEqual( `light_mode`, ); expect(renderComponentSpy.calls.allArgs()[1][0]).toBe(IconComponent); expect((renderComponentSpy.calls.allArgs()[1][1] as HTMLElement).innerText).toEqual( `dark_mode`, ); }); it('should render Breadcrumb component when content has element', async () => { navigationStateSpy.activeNavigationItem.and.returnValue({ label: 'Active Item', parent: { label: 'Parent Item', }, }); const fixture = TestBed.createComponent(DocViewer); const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any); fixture.componentRef.setInput('docContent', exampleContentWithBreadcrumbPlaceholder); await fixture.whenStable(); expect(renderComponentSpy).toHaveBeenCalledTimes(1); expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(Breadcrumb); }); it('should render copy source code buttons', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', exampleContentWithCodeSnippet); await fixture.whenStable(); const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton)); expect(copySourceCodeButton).toBeTruthy(); }); it('should render ToC', async () => { const fixture = TestBed.createComponent(DocViewer); const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any); fixture.componentRef.setInput('docContent', exampleContentWithHeadings); fixture.componentRef.setInput('hasToc', true); await fixture.whenStable(); expect(renderComponentSpy).toHaveBeenCalled(); expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(TableOfContents); }); it('should not render ToC when hasToc is false', async () => { const fixture = TestBed.createComponent(DocViewer); const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any); fixture.componentRef.setInput('docContent', exampleContentWithHeadings); fixture.componentRef.setInput('hasToc', false); await fixture.whenStable(); expect(renderComponentSpy).not.toHaveBeenCalled(); }); it('should setup copy link functionality for docs-anchor elements', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', exampleContentWithDocsAnchor); await fixture.whenStable(); const copyLinkButton = fixture.debugElement.query(By.directive(CopyLinkButton)); expect(copyLinkButton).toBeTruthy(); const anchor = fixture.nativeElement.querySelector('a.docs-anchor'); expect(anchor).toBeTruthy(); const copyButton = fixture.nativeElement.querySelector('docs-copy-link-button'); expect(copyButton).toBeTruthy(); }); it('should copy link to clipboard when copy button is clicked', async () => { const clipboard = TestBed.inject(Clipboard); const clipboardSpy = spyOn(clipboard, 'copy').and.returnValue(true); const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', exampleContentWithDocsAnchor); await fixture.whenStable(); const copyButton = fixture.nativeElement.querySelector('docs-copy-link-button'); copyButton.click(); expect(clipboardSpy).toHaveBeenCalled(); // Because the copyButton click bubbles up to an anchor tag, causing a navigation, it is // necessary to undo this location change by going back in the history. window.history.back(); }); });