import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; import {createGenericTestComponent} from '../test/common'; import {Component} from '@angular/core'; import {NgbTabsetModule} from './tabset.module'; import {NgbTabsetConfig} from './tabset-config'; import {NgbTabset} from './tabset'; const createTestComponent = (html: string) => createGenericTestComponent(html, TestComponent) as ComponentFixture; function getTabTitles(nativeEl: HTMLElement) { return nativeEl.querySelectorAll('.nav-link'); } function getTabContent(nativeEl: HTMLElement) { return nativeEl.querySelectorAll('.tab-content .tab-pane'); } function expectTabs(nativeEl: HTMLElement, active: boolean[], disabled?: boolean[]) { const tabTitles = getTabTitles(nativeEl); const tabContent = getTabContent(nativeEl); const anyTabsActive = active.reduce((prev, curr) => prev || curr, false); expect(tabTitles.length).toBe(active.length); expect(tabContent.length).toBe(anyTabsActive ? 1 : 0); // only 1 tab content in DOM at a time if (disabled) { expect(disabled.length).toBe(active.length); } else { disabled = new Array(active.length); // tabs are not disabled by default } for (let i = 0; i < active.length; i++) { if (active[i]) { expect(tabTitles[i]).toHaveCssClass('active'); } else { expect(tabTitles[i]).not.toHaveCssClass('active'); } if (disabled[i]) { expect(tabTitles[i]).toHaveCssClass('disabled'); expect(tabTitles[i].getAttribute('aria-disabled')).toBe('true'); expect(tabTitles[i].getAttribute('tabindex')).toBe('-1'); } else { expect(tabTitles[i]).not.toHaveCssClass('disabled'); expect(tabTitles[i].getAttribute('aria-disabled')).toBe('false'); expect(tabTitles[i].getAttribute('tabindex')).toBeNull(); } } } function getButton(nativeEl: HTMLElement) { return nativeEl.querySelectorAll('button'); } describe('ngb-tabset', () => { beforeEach( () => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbTabsetModule.forRoot()]}); }); it('should initialize inputs with default values', () => { const defaultConfig = new NgbTabsetConfig(); const tabset = new NgbTabset(new NgbTabsetConfig()); expect(tabset.type).toBe(defaultConfig.type); }); it('should render tabs and select first tab as active by default', () => { const fixture = createTestComponent(` Foo Bar `); const tabTitles = getTabTitles(fixture.nativeElement); const tabContent = getTabContent(fixture.nativeElement); expect(tabTitles[0].textContent).toMatch(/foo/); expect(tabTitles[1].textContent).toMatch(/bar/); expect(tabContent.length).toBe(1); expect(tabContent[0].textContent).toMatch(/Foo/); expectTabs(fixture.nativeElement, [true, false]); }); it('should have aria attributes', () => { const fixture = createTestComponent(` Foo Bar `); const compiled: HTMLElement = fixture.nativeElement; const tabTitles = getTabTitles(compiled); const tabContent = getTabContent(compiled); expect(tabTitles[0].getAttribute('role')).toBe('tab'); expect(tabTitles[0].getAttribute('aria-expanded')).toBe('true'); expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].getAttribute('id')); expect(tabContent[0].getAttribute('role')).toBe('tabpanel'); expect(tabContent[0].getAttribute('aria-expanded')).toBe('true'); expect(tabContent[0].getAttribute('aria-labelledby')).toBe(tabTitles[0].id); expect(tabTitles[1].getAttribute('role')).toBe('tab'); expect(tabTitles[1].getAttribute('aria-expanded')).toBe('false'); expect(tabTitles[1].getAttribute('aria-controls')).toBeNull(); }); it('should remove aria-controls when tab content is not in DOM', () => { const fixture = createTestComponent(` Foo Bar `); const compiled: HTMLElement = fixture.nativeElement; const tabTitles = getTabTitles(compiled); const tabContent = getTabContent(compiled); expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].getAttribute('id')); expect(tabContent[0].getAttribute('aria-expanded')).toBe('true'); expect(tabTitles[1].getAttribute('aria-controls')).toBeNull(); expect(tabContent[1]).toBeUndefined(); }); it('should have aria-controls and aria-expanded when tab content is hidden', () => { const fixture = createTestComponent(` Foo Bar `); const compiled: HTMLElement = fixture.nativeElement; const tabTitles = getTabTitles(compiled); const tabContent = getTabContent(compiled); expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].id); expect(tabContent[0].getAttribute('aria-expanded')).toBe('true'); expect(tabTitles[1].getAttribute('aria-controls')).toBe(tabContent[1].id); expect(tabContent[1].getAttribute('aria-expanded')).toBe('false'); }); it('should allow mix of text and HTML in tab titles', () => { const fixture = createTestComponent(` Foo bar Bar baz Baz `); const tabTitles = getTabTitles(fixture.nativeElement); expect(tabTitles[0].textContent).toMatch(/foo/); expect(tabTitles[1].innerHTML).toMatch(/bar<\/b>/); expect(tabTitles[2].textContent).toMatch(/bazbaz/); }); it('should not pick up titles from nested tabsets', () => { const testHtml = ` child `; const fixture = createTestComponent(testHtml); // additional change detection is required to reproduce the problem in the test environment fixture.detectChanges(); const titles = getTabTitles(fixture.nativeElement); const parentTitle = titles[0].textContent.trim(); const childTitle = titles[1].textContent.trim(); expect(parentTitle).toContain('parent'); expect(parentTitle).not.toContain('child'); expect(childTitle).toContain('child'); expect(childTitle).not.toContain('parent'); }); it('should not crash for empty tabsets', () => { const fixture = createTestComponent(``); expectTabs(fixture.nativeElement, []); }); it('should not crash for tabsets with empty tab content', () => { const fixture = createTestComponent(``); expectTabs(fixture.nativeElement, [true]); }); it('should mark the requested tab as active', () => { const fixture = createTestComponent(` Foo Bar `); expectTabs(fixture.nativeElement, [false, true]); }); it('should auto-correct requested active tab id', () => { const fixture = createTestComponent(` Foo Bar `); expectTabs(fixture.nativeElement, [true, false]); }); it('should auto-correct requested active tab id for undefined ids', () => { const fixture = createTestComponent(` Foo Bar `); expectTabs(fixture.nativeElement, [true, false]); }); it('should change active tab on tab title click', () => { const fixture = createTestComponent(` Foo Bar `); const tabTitles = getTabTitles(fixture.nativeElement); (tabTitles[1]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [false, true]); (tabTitles[0]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [true, false]); }); it('should support disabled tabs', () => { const fixture = createTestComponent(` Foo Bar `); expectTabs(fixture.nativeElement, [true, false], [false, true]); }); it('should not change active tab on disabled tab title click', () => { const fixture = createTestComponent(` Foo Bar `); expectTabs(fixture.nativeElement, [true, false], [false, true]); (getTabTitles(fixture.nativeElement)[1]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [true, false], [false, true]); }); it('should allow initially active and disabled tabs', () => { const fixture = createTestComponent(` Bar `); expectTabs(fixture.nativeElement, [true], [true]); }); it('should have nav-tabs default', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-tabs'); expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-pills'); }); it('should have pills upon setting pills', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-pills'); expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-tabs'); }); it('should have the "justify-content-start" class by default', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-start'); }); it('should have the "justify-content-center" class upon setting justify to center', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-center'); }); it('should have the "justify-content-end" upon setting justify to end', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-end'); }); it('should have the "nav-fill" class upon setting justify to fill', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-fill'); }); it('should have the "nav-justified" class upon setting justify to justified', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-justified'); }); it('should have the "justify-content-start" class upon setting orientation to horizontal', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('flex-column'); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-start'); }); it('should have the "flex-column" class upon setting orientation to vertical', () => { const fixture = createTestComponent(` Bar `); expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('flex-column'); expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('justify-content-start'); }); it('should change active tab by calling select on an exported directive instance', () => { const fixture = createTestComponent(` Foo Bar `); const button = getButton(fixture.nativeElement); // Click on a button to select the second tab (button[1]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [false, true]); // Click on a button to select the first tab (button[0]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [true, false]); }); it('should not change active tab by calling select on an exported directive instance in case of disable tab', () => { const fixture = createTestComponent(` Foo Bar `); const button = getButton(fixture.nativeElement); // Click on a button to select the second disabled tab (should not change active tab). (button[0]).click(); fixture.detectChanges(); expectTabs(fixture.nativeElement, [true, false], [false, true]); }); it('should not remove inactive tabs content from DOM with `destroyOnHide` flag', () => { const fixture = createTestComponent(` Foo Bar `); const button = getButton(fixture.nativeElement); // Click on a button to select the second tab (button[0]).click(); fixture.detectChanges(); let tabContents = getTabContent(fixture.nativeElement); expect(tabContents.length).toBe(2); expect(tabContents[1]).toHaveCssClass('active'); }); it('should emit tab change event when switching tabs', () => { const fixture = createTestComponent(` First Second `); const button = getButton(fixture.nativeElement); spyOn(fixture.componentInstance, 'changeCallback'); // Select the second tab -> change event (button[1]).click(); fixture.detectChanges(); expect(fixture.componentInstance.changeCallback) .toHaveBeenCalledWith(jasmine.objectContaining({activeId: 'first', nextId: 'second'})); // Select the first tab again -> change event (button[0]).click(); fixture.detectChanges(); expect(fixture.componentInstance.changeCallback) .toHaveBeenCalledWith(jasmine.objectContaining({activeId: 'second', nextId: 'first'})); }); it('should not emit tab change event when selecting currently active and disabled tabs', () => { const fixture = createTestComponent(` First Second `); const button = getButton(fixture.nativeElement); spyOn(fixture.componentInstance, 'changeCallback'); // Select the currently active tab -> no change event (button[0]).click(); fixture.detectChanges(); expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); // Select the disabled tab -> no change event (button[1]).click(); fixture.detectChanges(); expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); }); it('should cancel tab change when preventDefault() is called', () => { const fixture = createTestComponent(` First Second `); const button = getButton(fixture.nativeElement); let changeEvent = null; fixture.componentInstance.changeCallback = (event) => { changeEvent = event; event.preventDefault(); }; // Select the second tab -> selection will be canceled (button[1]).click(); fixture.detectChanges(); expect(changeEvent).toEqual(jasmine.objectContaining({activeId: 'first', nextId: 'second'})); expectTabs(fixture.nativeElement, [true, false]); }); describe('Custom config', () => { let config: NgbTabsetConfig; beforeEach(() => { TestBed.configureTestingModule({imports: [NgbTabsetModule.forRoot()]}); }); beforeEach(inject([NgbTabsetConfig], (c: NgbTabsetConfig) => { config = c; config.type = 'pills'; })); it('should initialize inputs with provided config', () => { const fixture = TestBed.createComponent(NgbTabset); fixture.detectChanges(); let tabset = fixture.componentInstance; expect(tabset.type).toBe(config.type); }); }); describe('Custom config as provider', () => { let config = new NgbTabsetConfig(); config.type = 'pills'; beforeEach(() => { TestBed.configureTestingModule( {imports: [NgbTabsetModule.forRoot()], providers: [{provide: NgbTabsetConfig, useValue: config}]}); }); it('should initialize inputs with provided config as provider', () => { const fixture = TestBed.createComponent(NgbTabset); fixture.detectChanges(); let tabset = fixture.componentInstance; expect(tabset.type).toBe(config.type); }); }); }); @Component({selector: 'test-cmp', template: ''}) class TestComponent { activeTabId: string; changeCallback = (event: any) => {}; }