/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.spec.ts
289 строк
17 KB
Kam
fix(docs-infra): keep copy link icon out of the search index
06 июл 2026, 23:39
06 июл 2026, 23:39
e4ffb24
Код
Авторство
О чём код?
/*! * @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'; import {isFirefox} from '../../../utils'; describe('DocViewer', () => { let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>; let navigationStateSpy: jasmine.SpyObj<NavigationState>; const exampleDocContentWithExampleViewerPlaceholders = `<div class="docs-code linenums" visibleLines="[12, 31]" expanded="true" path="hello-world/hello-world-new.ts"> <div class="docs-code-header">A styled code example</div> <pre> <code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line highlighted">@Component({</div><div class="hljs-ln-line highlighted"> selector: <span class="hljs-string">'hello-world'</span>,</div><div class="hljs-ln-line highlighted"> imports: [CommonModule],</div><div class="hljs-ln-line highlighted"> templateUrl: <span class="hljs-string">'./hello-world.html'</span>,</div><div class="hljs-ln-line highlighted"> styleUrls: [<span class="hljs-string">'./hello-world.css'</span>],</div><div class="hljs-ln-line highlighted">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">'World'</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">'World!!!'</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) => {</div><div class="hljs-ln-line highlighted"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code> </pre> </div>`; const exampleDocContentWithExpandedExampleViewerPlaceholders = `<div class="docs-code-multifile" expanded="true" path="hello-world/hello-world-new.ts"> <div class="docs-code" visibleLines="[12, 31]" path="hello-world/hello-world-new.ts"> <pre> <code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line">@Component({</div><div class="hljs-ln-line"> selector: <span class="hljs-string">'hello-world'</span>,</div><div class="hljs-ln-line"> imports: [CommonModule],</div><div class="hljs-ln-line"> templateUrl: <span class="hljs-string">'./hello-world.html'</span>,</div><div class="hljs-ln-line"> styleUrls: [<span class="hljs-string">'./hello-world.css'</span>],</div><div class="hljs-ln-line">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">'World'</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">'World!!!'</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) => {</div><div class="hljs-ln-line"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code> </pre> </div> <div class="docs-code linenums" path="hello-world/hello-world.html"> <pre> <code><div class="hljs-ln-line"><span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">h2</span>></span>Hello </span><span class="hljs-template-variable">{{ <span class="hljs-name">world</span> }}</span><span class="language-xml"><span class="hljs-tag"></<span class="hljs-name">h2</span>></span></div><div class="hljs-ln-line"><span class="hljs-tag"><<span class="hljs-name">button</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"increase()"</span>></span>Increase<span class="hljs-tag"></<span class="hljs-name">button</span>></span></div><div class="hljs-ln-line"><span class="hljs-tag"><<span class="hljs-name">p</span>></span>Counter: </span><span class="hljs-template-variable">{{ <span class="hljs-name">count</span>() }}</span><span class="language-xml"><span class="hljs-tag"></<span class="hljs-name">p</span>></span></div><div class="hljs-ln-line"></span></div></code> </pre> </div> </div>`; const exampleContentWithIcons = ` <p>Content</p> <docs-icon>light_mode</docs-icon> <p>More content</p> <docs-icon>dark_mode</docs-icon> `; const exampleContentWithBreadcrumbPlaceholder = ` <docs-breadcrumb></docs-breadcrumb> <p>Content</p> `; const exampleContentWithCodeSnippet = ` <div class="docs-code" path="forms/src/app/actor.ts" header="src/app/actor.ts"> <code> <div class="hljs-ln-line"></div> </code> </div> `; const exampleContentWithVideoFacade = ` <div class="docs-video-container"> <a class="docs-video-facade" href="https://www.youtube.com/watch?v=abc123&autoplay=1" target="_blank" rel="noopener" aria-label="Play video: Test video" data-video-src="https://www.youtube.com/embed/abc123" data-video-title="Test video" > <img class="docs-video-thumbnail" src="https://i.ytimg.com/vi/abc123/maxresdefault.jpg" alt="" loading="lazy" /> <span class="docs-video-play-button" aria-hidden="true"></span> </a> </div> `; const exampleContentWithHeadings = ` <h2>Heading h2</h2> <h3>Heading h3</h3> `; const exampleContentWithDocsAnchor = ` <h2 id="test-section"> <a href="#test-section" class="docs-anchor" tabindex="-1" aria-label="Link to Test Section">Test Section</a> </h2> `; 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 <docs-icon> 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 <docs-breadcrumb> 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 not leak any icon text content (so it stays out of the search index)', () => { const fixture = TestBed.createComponent(CopyLinkButton); fixture.componentRef.setInput('href', '#test-section'); fixture.componentRef.setInput('label', 'Test Section'); fixture.componentRef.setInput('matTooltip', 'Copy link to Test Section'); fixture.detectChanges(); expect(fixture.nativeElement.textContent.trim()).toBe(''); }); 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(); }); it('should upgrade a video facade to an iframe in browsers that can embed it', async () => { const fixture = TestBed.createComponent(DocViewer); fixture.componentRef.setInput('docContent', exampleContentWithVideoFacade); await fixture.whenStable(); const iframe = fixture.nativeElement.querySelector('iframe.docs-video'); const facade = fixture.nativeElement.querySelector('a.docs-video-facade'); if (isFirefox) { // Firefox can't load the cross-origin embed under COEP, so the facade stays a link. expect(facade).toBeTruthy(); expect(iframe).toBeNull(); } else { expect(iframe).toBeTruthy(); expect(iframe.getAttribute('src')).toContain('youtube.com/embed/abc123'); expect(iframe.hasAttribute('credentialless')).toBeTrue(); expect(facade).toBeNull(); } }); });