diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-content-component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-content-component.spec.ts new file mode 100644 index 0000000000..bb5bf71456 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-content-component.spec.ts @@ -0,0 +1,43 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { SkyTabsetHarness } from '@skyux/tabs/testing'; + +import { RecordPageContentComponent } from './record-page-content.component'; +import { RecordPageOverviewTabHarness } from './record-page-overview-tab-harness'; + +describe('Record page content', () => { + async function setupTest(): Promise<{ + recordPageHarness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(RecordPageContentComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const recordPageHarness = await loader.getHarness(SkyTabsetHarness); + + return { recordPageHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RecordPageContentComponent, NoopAnimationsModule], + }); + }); + + it("should get the overview tab's content harness", async () => { + const { recordPageHarness } = await setupTest(); + + const overviewTabHarness = await ( + await recordPageHarness.getTabContentHarness('Overview') + ).queryHarness(RecordPageOverviewTabHarness); + + const overviewBoxes = await overviewTabHarness.getBoxes(); + + expect(overviewBoxes.length).toBe(3); + }); +}); diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab-harness.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab-harness.ts new file mode 100644 index 0000000000..73acb592cd --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab-harness.ts @@ -0,0 +1,10 @@ +import { ComponentHarness } from '@angular/cdk/testing'; +import { SkyBoxHarness } from '@skyux/layout/testing'; + +export class RecordPageOverviewTabHarness extends ComponentHarness { + public static hostSelector = 'app-record-page-overview-tab'; + + public async getBoxes(): Promise { + return await this.locatorForAll(SkyBoxHarness)(); + } +} diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.html b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.html index 4c9bcdd98a..1966ce4f3c 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.html +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.html @@ -1,4 +1,8 @@ - + @for (tab of tabArray; track tab; let i = $index) { {{ tab.tabContent }} diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.spec.ts new file mode 100644 index 0000000000..76c999850a --- /dev/null +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.spec.ts @@ -0,0 +1,50 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { SkyTabsetHarness } from '@skyux/tabs/testing'; + +import { DemoComponent } from './demo.component'; + +describe('Static tabs demo with add and close', () => { + async function setupTest(options: { dataSkyId?: string }): Promise<{ + harness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(DemoComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyTabsetHarness.with({ dataSkyId: options.dataSkyId }), + ); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [DemoComponent] }); + }); + + it('should set up tabs', async () => { + const { harness } = await setupTest({ dataSkyId: 'tab-demo' }); + + await harness.clickNewTabButton(); + const tabButtonHarnesses = await harness.getTabButtonHarnesses(); + expect(tabButtonHarnesses.length).toBe(4); + + const activeTab = await harness.getActiveTabButton(); + await expectAsync(activeTab?.getTabHeading()).toBeResolvedTo('Tab 1'); + }); + + it('should hide Tab 3 if it is closed', async () => { + const { harness } = await setupTest({ dataSkyId: 'tab-demo' }); + + const tab3Harness = await harness.getTabButtonHarness('Tab 3'); + await tab3Harness.clickRemoveButton(); + + const tabButtons = await harness.getTabButtonHarnesses(); + expect(tabButtons.length).toBe(2); + }); +}); diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.ts index 4677a5ed8a..cb13e25130 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic-add-close/demo.component.ts @@ -25,7 +25,7 @@ export class DemoComponent { #tabCounter = 3; - protected onNewTabClick(): void { + public onNewTabClick(): void { this.#tabCounter++; this.tabArray.push({ @@ -34,7 +34,7 @@ export class DemoComponent { }); } - protected onCloseClick(arrayIndex: number): void { + public onCloseClick(arrayIndex: number): void { this.tabArray.splice(arrayIndex, 1); } } diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.html b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.html index eabad39c03..a8f5ce1ecb 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.html @@ -1,4 +1,4 @@ - + @for (tab of tabArray; track tab) { {{ tab.tabContent }} diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.spec.ts new file mode 100644 index 0000000000..16d4e39469 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/dynamic/demo.component.spec.ts @@ -0,0 +1,39 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { SkyTabsetHarness } from '@skyux/tabs/testing'; + +import { DemoComponent } from './demo.component'; + +describe('Static tabs demo with add and close', () => { + async function setupTest(options: { dataSkyId?: string }): Promise<{ + harness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(DemoComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyTabsetHarness.with({ dataSkyId: options.dataSkyId }), + ); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [DemoComponent] }); + }); + + it('should set up tabs', async () => { + const { harness } = await setupTest({ dataSkyId: 'tab-demo' }); + + const tabButtonHarnesses = await harness.getTabButtonHarnesses(); + expect(tabButtonHarnesses.length).toBe(3); + + const activeTab = await harness.getActiveTabButton(); + await expectAsync(activeTab?.getTabHeading()).toBeResolvedTo('Tab 1'); + }); +}); diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.html b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.html index 5d83b37460..692401afb8 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.html +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.html @@ -1,4 +1,8 @@ - + Content for Tab 1 Content for Tab 2 @if (showTab3) { diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.spec.ts new file mode 100644 index 0000000000..81dd164eec --- /dev/null +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.spec.ts @@ -0,0 +1,54 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { SkyTabsetHarness } from '@skyux/tabs/testing'; + +import { DemoComponent } from './demo.component'; + +describe('Static tabs demo with add and close', () => { + async function setupTest(options: { dataSkyId?: string }): Promise<{ + harness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(DemoComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyTabsetHarness.with({ dataSkyId: options.dataSkyId }), + ); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [DemoComponent] }); + }); + + it('should set up tabs', async () => { + const { harness, fixture } = await setupTest({ dataSkyId: 'tab-demo' }); + + const spy = spyOn(fixture.componentInstance, 'onNewTabClick'); + await harness.clickNewTabButton(); + + expect(spy).toHaveBeenCalled(); + + const tabButtonHarnesses = await harness.getTabButtonHarnesses(); + expect(tabButtonHarnesses.length).toBe(3); + + const activeTab = await harness.getActiveTabButton(); + await expectAsync(activeTab?.getTabHeading()).toBeResolvedTo('Tab 1'); + }); + + it('should hide Tab 3 if it is closed', async () => { + const { harness } = await setupTest({ dataSkyId: 'tab-demo' }); + + const tab3Harness = await harness.getTabButtonHarness('Tab 3'); + await tab3Harness.clickRemoveButton(); + + const tabButtons = await harness.getTabButtonHarnesses(); + expect(tabButtons.length).toBe(2); + }); +}); diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.ts index ca0cb0904c..8e381490cf 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/static-add-close/demo.component.ts @@ -10,7 +10,7 @@ import { SkyTabsModule } from '@skyux/tabs'; export class DemoComponent { protected showTab3 = true; - protected onNewTabClick(): void { + public onNewTabClick(): void { alert('Add tab clicked!'); } } diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.html b/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.html index 9bf4513924..bc2aafaeeb 100644 --- a/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.html +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.html @@ -1,4 +1,4 @@ - + Content for Tab 1 Content for Tab 2 Content for Tab 3 diff --git a/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.spec.ts new file mode 100644 index 0000000000..16d4e39469 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/tabs/tabs/static/demo.component.spec.ts @@ -0,0 +1,39 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { SkyTabsetHarness } from '@skyux/tabs/testing'; + +import { DemoComponent } from './demo.component'; + +describe('Static tabs demo with add and close', () => { + async function setupTest(options: { dataSkyId?: string }): Promise<{ + harness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(DemoComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyTabsetHarness.with({ dataSkyId: options.dataSkyId }), + ); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [DemoComponent] }); + }); + + it('should set up tabs', async () => { + const { harness } = await setupTest({ dataSkyId: 'tab-demo' }); + + const tabButtonHarnesses = await harness.getTabButtonHarnesses(); + expect(tabButtonHarnesses.length).toBe(3); + + const activeTab = await harness.getActiveTabButton(); + await expectAsync(activeTab?.getTabHeading()).toBeResolvedTo('Tab 1'); + }); +}); diff --git a/apps/playground/src/app/components/tabs/tabs/tabs.component.html b/apps/playground/src/app/components/tabs/tabs/tabs.component.html index 0030fa05f5..59d8f54145 100644 --- a/apps/playground/src/app/components/tabs/tabs/tabs.component.html +++ b/apps/playground/src/app/components/tabs/tabs/tabs.component.html @@ -1,47 +1,11 @@ - -
- - - Tab 1 Content - - - Tab 2 Content - - - Tab 3 Content - - - Tab 4 Content - - - This tab cannot be closed - - - Tab 3 Content - - -
- -
- - - Tab 1 Content - - Tab 2 Content - -
-
+ + + Content for Tab 1 + Content for Tab 2 + @if (showTab3) { + + Content for Tab 3 + + } + diff --git a/apps/playground/src/app/components/tabs/tabs/tabs.component.ts b/apps/playground/src/app/components/tabs/tabs/tabs.component.ts index 4a9ea0b40c..0925de5033 100644 --- a/apps/playground/src/app/components/tabs/tabs/tabs.component.ts +++ b/apps/playground/src/app/components/tabs/tabs/tabs.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyTabIndex } from '@skyux/tabs'; @Component({ selector: 'app-tabs', @@ -6,9 +7,10 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TabsComponent { - public newTabClick() {} + protected showTab3 = true; + protected tabIndexValue: SkyTabIndex = '2'; - public openTabClick() {} - - public closeTab() {} + protected onNewTabClick(): void { + alert('Add tab clicked!'); + } } diff --git a/libs/components/popovers/testing/src/modules/dropdown/dropdown-menu-harness.ts b/libs/components/popovers/testing/src/modules/dropdown/dropdown-menu-harness.ts index 3aa733557f..2fcdbe4b0c 100644 --- a/libs/components/popovers/testing/src/modules/dropdown/dropdown-menu-harness.ts +++ b/libs/components/popovers/testing/src/modules/dropdown/dropdown-menu-harness.ts @@ -1,5 +1,8 @@ import { HarnessPredicate } from '@angular/cdk/testing'; -import { SkyComponentHarness, SkyOverlayHarness } from '@skyux/core/testing'; +import { + SkyOverlayHarness, + SkyQueryableComponentHarness, +} from '@skyux/core/testing'; import { SkyDropdownItemHarness } from './dropdown-item-harness'; import { SkyDropdownItemHarnessFilters } from './dropdown-item-harness.filters'; @@ -8,7 +11,7 @@ import { SkyDropdownMenuHarnessFilters } from './dropdown-menu-harness.filters'; /** * Harness for interacting with dropdown menu in tests. */ -export class SkyDropdownMenuHarness extends SkyComponentHarness { +export class SkyDropdownMenuHarness extends SkyQueryableComponentHarness { /** * @internal */ diff --git a/libs/components/tabs/package.json b/libs/components/tabs/package.json index eb02b7ab0e..7a9b3bd18e 100644 --- a/libs/components/tabs/package.json +++ b/libs/components/tabs/package.json @@ -17,6 +17,7 @@ "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { "@angular/animations": "^18.2.13", + "@angular/cdk": "^18.2.14", "@angular/common": "^18.2.13", "@angular/core": "^18.2.13", "@angular/platform-browser": "^18.2.13", diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.component.html b/libs/components/tabs/src/lib/modules/tabs/tabset.component.html index f1297c3cfd..7a412fb55d 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.component.html +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.component.html @@ -1,3 +1,4 @@ +
{ + return new HarnessPredicate(SkyTabButtonHarness, filters).addOption( + 'tabHeading', + filters.tabHeading, + async (harness, tabHeading) => { + const harnessTabHeading = await harness.getTabHeading(); + return await HarnessPredicate.stringMatches( + harnessTabHeading, + tabHeading, + ); + }, + ); + } + + /** + * Clicks the tab button. + */ + public async click(): Promise { + return await (await this.#getTabButton()).click(); + } + + /** + * Clicks the remove tab button if it is visible. + */ + public async clickRemoveButton(): Promise { + const button = await this.locatorForOptional('.sky-btn-tab-close')(); + if (!button) { + throw new Error('Unable to find remove tab button.'); + } + return await button.click(); + } + + /** + * Gets the permalink that the page routes to when the tab is clicked. + */ + public async getPermalink(): Promise { + return await (await this.#getTabButton()).getAttribute('href'); + } + + /** + * Gets the `SkyTabContentHarness` controlled by this tab button. + */ + public async getTabContentHarness(): Promise { + return await this.documentRootLocatorFactory().locatorFor( + SkyTabContentHarness.with({ tabId: await this.getTabId() }), + )(); + } + + /** + * Gets the tab heading. + */ + public async getTabHeading(): Promise { + return ( + // eslint-disable-next-line @cspell/spellchecker + await (await this.locatorFor('.sky-tab-heading > span[skyid]')()).text() + ); + } + + /** + * Gets whether the tab button is active. + */ + public async isActive(): Promise { + return await (await this.#getTabButton()).hasClass('sky-btn-tab-selected'); + } + + /** + * Gets whether the tab button is disabled. + */ + public async isDisabled(): Promise { + return await (await this.#getTabButton()).hasClass('sky-btn-tab-disabled'); + } + + /** + * @internal + */ + public async getTabId(): Promise { + return ( + (await (await this.#getTabButton()).getAttribute('aria-controls')) || + /* istanbul ignore next */ + '' + ); + } +} diff --git a/libs/components/tabs/testing/src/modules/tabs/tab-content-harness-filters.ts b/libs/components/tabs/testing/src/modules/tabs/tab-content-harness-filters.ts new file mode 100644 index 0000000000..0b35c84aa0 --- /dev/null +++ b/libs/components/tabs/testing/src/modules/tabs/tab-content-harness-filters.ts @@ -0,0 +1,13 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyTabContentHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyTabContentHarnessFilters extends SkyHarnessFilters { + /** + * Finds tabs whose id matches given value. + * @internal + */ + tabId: string; +} diff --git a/libs/components/tabs/testing/src/modules/tabs/tab-content-harness.ts b/libs/components/tabs/testing/src/modules/tabs/tab-content-harness.ts new file mode 100644 index 0000000000..b58defb38f --- /dev/null +++ b/libs/components/tabs/testing/src/modules/tabs/tab-content-harness.ts @@ -0,0 +1,55 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyQueryableComponentHarness } from '@skyux/core/testing'; + +import { SkyTabContentHarnessFilters } from './tab-content-harness-filters'; + +/** + * Harness for interacting with a tab component in tests. + */ +export class SkyTabContentHarness extends SkyQueryableComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-tab'; + + #getTab = this.locatorFor('.sky-tab'); + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyTabContentHarness` that meets certain criteria. + * @internal + */ + public static with( + filters: SkyTabContentHarnessFilters, + ): HarnessPredicate { + return new HarnessPredicate(SkyTabContentHarness, filters).addOption( + 'tabId', + filters.tabId, + async (harness, tabId) => { + const harnessId = await harness.getTabId(); + return await HarnessPredicate.stringMatches(harnessId, tabId); + }, + ); + } + + /** + * Gets the tab's layout. + */ + public async getLayout(): Promise { + return (await (await this.host()).getAttribute('layout')) || 'none'; + } + + /** + * @internal + */ + public async getTabId(): Promise { + return await (await this.#getTab()).getAttribute('id'); + } + + /** + * Whether the tab content is visible. + */ + public async isVisible(): Promise { + return !(await (await this.#getTab()).getProperty('hidden')); + } +} diff --git a/libs/components/tabs/testing/src/modules/tabs/tabset-harness-filters.ts b/libs/components/tabs/testing/src/modules/tabs/tabset-harness-filters.ts new file mode 100644 index 0000000000..44657ff03e --- /dev/null +++ b/libs/components/tabs/testing/src/modules/tabs/tabset-harness-filters.ts @@ -0,0 +1,7 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyTabsetHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyTabsetHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/tabs/testing/src/modules/tabs/tabset-harness.spec.ts b/libs/components/tabs/testing/src/modules/tabs/tabset-harness.spec.ts new file mode 100644 index 0000000000..19a78f8ea3 --- /dev/null +++ b/libs/components/tabs/testing/src/modules/tabs/tabset-harness.spec.ts @@ -0,0 +1,314 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; +import { SkyPageModule } from '@skyux/pages'; +import { SkyTabIndex, SkyTabsModule } from '@skyux/tabs'; + +import { SkyTabButtonHarness } from './tab-button-harness'; +import { SkyTabsetHarness } from './tabset-harness'; + +@Component({ + standalone: true, + imports: [SkyTabsModule, SkyPageModule], + template: ` + + + Tab 1 Content + + Tab 2 Content + Tab 3 Content + + + + + + + + + + `, +}) +class TestComponent { + public active = false; + public ariaLabel: string | undefined; + public ariaLabelledBy: string | undefined; + public permalinkId: string | undefined; + public permalinkValue: string | undefined; + public tabHeading: string | undefined = 'Tab 1'; + public tabIndexValue: SkyTabIndex | undefined; + + public tabAction(): void { + // This function is for the spy. + } +} + +describe('Tab harness', () => { + async function setupTest(options: { dataSkyId?: string } = {}): Promise<{ + tabsetHarness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(TestComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const tabsetHarness: SkyTabsetHarness = options.dataSkyId + ? await loader.getHarness( + SkyTabsetHarness.with({ dataSkyId: options.dataSkyId }), + ) + : await loader.getHarness(SkyTabsetHarness); + + return { tabsetHarness, fixture }; + } + + it('should click the new tab button', async () => { + const { tabsetHarness, fixture } = await setupTest(); + const newTabClickSpy = spyOn(fixture.componentInstance, 'tabAction'); + + await tabsetHarness.clickNewTabButton(); + + expect(newTabClickSpy).toHaveBeenCalled(); + }); + + it('should throw an error if attempting to click new tab if it is not enabled', async () => { + const { tabsetHarness } = await setupTest({ + dataSkyId: 'other-tabset', + }); + + await expectAsync(tabsetHarness.clickNewTabButton()).toBeRejectedWithError( + 'Unable to find the new tab button.', + ); + }); + + it('should click the open tab button', async () => { + const { tabsetHarness, fixture } = await setupTest(); + const openTabClickSpy = spyOn(fixture.componentInstance, 'tabAction'); + + await tabsetHarness.clickOpenTabButton(); + + expect(openTabClickSpy).toHaveBeenCalled(); + }); + + it('should throw an error if attempting to click open tab if it is not enabled', async () => { + const { tabsetHarness } = await setupTest({ + dataSkyId: 'other-tabset', + }); + + await expectAsync(tabsetHarness.clickOpenTabButton()).toBeRejectedWithError( + 'Unable to find the open tab button.', + ); + }); + + it('should get aria-label', async () => { + const { tabsetHarness, fixture } = await setupTest(); + fixture.componentInstance.ariaLabel = 'aria label'; + fixture.detectChanges(); + + await expectAsync(tabsetHarness.getAriaLabel()).toBeResolvedTo( + 'aria label', + ); + }); + + it('should get aria-labelledby', async () => { + const { tabsetHarness, fixture } = await setupTest(); + fixture.componentInstance.ariaLabelledBy = 'aria-labelledby'; + fixture.detectChanges(); + + await expectAsync(tabsetHarness.getAriaLabelledBy()).toBeResolvedTo( + 'aria-labelledby', + ); + }); + + it('should get a tab button harness by tab heading', async () => { + const { tabsetHarness } = await setupTest(); + const tabOne = await tabsetHarness.getTabButtonHarness('Tab 1'); + + await expectAsync(tabOne.getTabHeading()).toBeResolvedTo('Tab 1'); + }); + + it('should get all tab button harnesses', async () => { + const { tabsetHarness } = await setupTest(); + const tabs = await tabsetHarness.getTabButtonHarnesses(); + + expect(tabs.length).toBe(4); + }); + + it('should get the active tab button', async () => { + const { tabsetHarness } = await setupTest(); + const activeTab = await tabsetHarness.getActiveTabButton(); + + await expectAsync(activeTab?.getTabHeading()).toBeResolvedTo('Tab 1'); + }); + + it('should get the mode when tab', async () => { + const { tabsetHarness } = await setupTest(); + + await expectAsync(tabsetHarness.getMode()).toBeResolvedTo('tabs'); + }); + + it('should get the mode when dropdown', async () => { + const { tabsetHarness, fixture } = await setupTest(); + fixture.nativeElement.style.width = '50px'; + SkyAppTestUtility.fireDomEvent(window, 'resize'); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(tabsetHarness.getMode()).toBeResolvedTo('dropdown'); + }); + + it('should get the tab content harness from tab heading', async () => { + const { tabsetHarness } = await setupTest(); + let tabContentHarness = await tabsetHarness.getTabContentHarness('Tab 1'); + await expectAsync(tabContentHarness.isVisible()).toBeResolvedTo(true); + tabContentHarness = await tabsetHarness.getTabContentHarness('Tab 2'); + await expectAsync(tabContentHarness.isVisible()).toBeResolvedTo(false); + }); + + it('should get the tab layout in pages', async () => { + const { tabsetHarness } = await setupTest({ dataSkyId: 'other-tabset' }); + const tabContentHarness = await tabsetHarness.getTabContentHarness('Tab 1'); + await expectAsync(tabContentHarness.getLayout()).toBeResolvedTo('blocks'); + }); + + it('should get the default tab layout', async () => { + const { tabsetHarness } = await setupTest(); + const tabContentHarness = await tabsetHarness.getTabContentHarness('Tab 1'); + await expectAsync(tabContentHarness.getLayout()).toBeResolvedTo('none'); + }); + + it('should click a tab', async () => { + const { tabsetHarness } = await setupTest(); + await expectAsync( + (await tabsetHarness.getActiveTabButton())?.getTabHeading(), + ).toBeResolvedTo('Tab 1'); + + await tabsetHarness.clickTabButton('Tab 2'); + await expectAsync( + (await tabsetHarness.getActiveTabButton())?.getTabHeading(), + ).toBeResolvedTo('Tab 2'); + }); + + describe('tab button harness', () => { + async function setupTabButtonTest(tabHeading: string): Promise<{ + tabButtonHarness: SkyTabButtonHarness; + tabsetHarness: SkyTabsetHarness; + fixture: ComponentFixture; + }> { + const { tabsetHarness, fixture } = await setupTest(); + const tabButtonHarness = + await tabsetHarness.getTabButtonHarness(tabHeading); + return { tabButtonHarness, tabsetHarness, fixture }; + } + + it('should click the tab button and change the active tab', async () => { + const { tabButtonHarness } = await setupTabButtonTest('Tab 2'); + await expectAsync(tabButtonHarness.isActive()).toBeResolvedTo(false); + await tabButtonHarness.click(); + await expectAsync(tabButtonHarness.isActive()).toBeResolvedTo(true); + }); + + it('should get the permalink for a tab button', async () => { + const { tabButtonHarness, fixture } = await setupTabButtonTest('Tab 1'); + fixture.componentInstance.permalinkId = 'test-tab'; + fixture.componentInstance.permalinkValue = 'tab-1'; + fixture.detectChanges(); + + await expectAsync(tabButtonHarness.getPermalink()).toBeResolvedTo( + '/?test-tab-active-tab=tab-1', + ); + }); + + it('should get when tab is disabled', async () => { + const { tabButtonHarness } = await setupTabButtonTest('Disabled tab'); + await expectAsync(tabButtonHarness.isDisabled()).toBeResolvedTo(true); + }); + + it('should get when tab is not disabled', async () => { + const { tabButtonHarness } = await setupTabButtonTest('Tab 1'); + await expectAsync(tabButtonHarness.isDisabled()).toBeResolvedTo(false); + }); + + it('should get a tab content harness', async () => { + const { tabButtonHarness } = await setupTabButtonTest('Tab 1'); + const tabContentHarness = await tabButtonHarness.getTabContentHarness(); + await expectAsync(tabContentHarness.isVisible()).toBeResolvedTo(true); + }); + + it('should throw an error if trying to click dropdown when not in dropdown mode', async () => { + const { tabsetHarness } = await setupTest(); + await expectAsync(tabsetHarness.clickDropdownTab()).toBeRejectedWithError( + 'Cannot click dropdown tab button, tab is not in dropdown mode.', + ); + }); + + it('should throw an error if trying to close a tab with no close button', async () => { + const { tabButtonHarness } = await setupTabButtonTest('Tab 2'); + await expectAsync( + tabButtonHarness.clickRemoveButton(), + ).toBeRejectedWithError('Unable to find remove tab button.'); + }); + + it('should click close button', async () => { + const { tabButtonHarness, fixture } = await setupTabButtonTest('Tab 1'); + const closeClickSpy = spyOn(fixture.componentInstance, 'tabAction'); + await tabButtonHarness.clickRemoveButton(); + expect(closeClickSpy).toHaveBeenCalled(); + }); + }); + + describe('in dropdown mode', () => { + async function shrinkScreen( + fixture: ComponentFixture, + ): Promise { + fixture.nativeElement.style.width = '50px'; + SkyAppTestUtility.fireDomEvent(window, 'resize'); + fixture.detectChanges(); + await fixture.whenStable(); + } + it('should click dropdown tab', async () => { + const { tabsetHarness, fixture } = await setupTest(); + await shrinkScreen(fixture); + await expectAsync(tabsetHarness.clickDropdownTab()).toBeResolved(); + }); + + it('should get a tab button harness from tab heading', async () => { + const { tabsetHarness, fixture } = await setupTest(); + await shrinkScreen(fixture); + await tabsetHarness.clickDropdownTab(); + const tabButtonHarness = await tabsetHarness.getTabButtonHarness('Tab 1'); + + await expectAsync(tabButtonHarness.isActive()).toBeResolvedTo(true); + }); + + it('should get all tab button harnesses', async () => { + const { tabsetHarness, fixture } = await setupTest(); + await shrinkScreen(fixture); + await tabsetHarness.clickDropdownTab(); + const tabButtonHarnesses = await tabsetHarness.getTabButtonHarnesses(); + expect(tabButtonHarnesses.length).toBe(4); + }); + + it('should throw an error when trying to get tab buttons when dropdown is closed', async () => { + const { tabsetHarness, fixture } = await setupTest(); + await shrinkScreen(fixture); + await expectAsync( + tabsetHarness.getTabButtonHarnesses(), + ).toBeRejectedWithError( + 'Cannot get tab button when tabs is in dropdown mode and is closed.', + ); + }); + }); +}); diff --git a/libs/components/tabs/testing/src/modules/tabs/tabset-harness.ts b/libs/components/tabs/testing/src/modules/tabs/tabset-harness.ts new file mode 100644 index 0000000000..07700aff07 --- /dev/null +++ b/libs/components/tabs/testing/src/modules/tabs/tabset-harness.ts @@ -0,0 +1,175 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; +import { + SkyDropdownHarness, + SkyDropdownMenuHarness, +} from '@skyux/popovers/testing'; +import { SkyTabsetButtonsDisplayMode } from '@skyux/tabs'; + +import { SkyTabButtonHarness } from './tab-button-harness'; +import { SkyTabContentHarness } from './tab-content-harness'; +import { SkyTabsetHarnessFilters } from './tabset-harness-filters'; + +/** + * Harness for interacting with a tabset component in tests. + */ +export class SkyTabsetHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-tabset'; + + #getTabset = this.locatorFor('.sky-tabset'); + #getTabButtons = this.locatorForAll(SkyTabButtonHarness); + #getTabDropdown = this.locatorFor(SkyDropdownHarness); + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyTabsetHarness` that meets certain criteria. + */ + public static with( + filters: SkyTabsetHarnessFilters, + ): HarnessPredicate { + return SkyTabsetHarness.getDataSkyIdPredicate(filters); + } + + /** + * In `dropdown` mode, clicks the dropdown tab to open the tab dropdown menu. + */ + public async clickDropdownTab(): Promise { + if (!((await this.getMode()) === 'dropdown')) { + throw new Error( + 'Cannot click dropdown tab button, tab is not in dropdown mode.', + ); + } + const button = await this.locatorFor(SkyDropdownHarness)(); + return await button.clickDropdownButton(); + } + + /** + * Clicks the new tab button if visible. + */ + public async clickNewTabButton(): Promise { + const newTabButton = await this.locatorForOptional( + 'button.sky-tabset-btn-new', + )(); + + if (!newTabButton) { + throw new Error('Unable to find the new tab button.'); + } + + return await newTabButton.click(); + } + + /** + * Clicks the open tab button if visible. + */ + public async clickOpenTabButton(): Promise { + const openTabButton = await this.locatorForOptional( + 'button.sky-tabset-btn-open', + )(); + + if (!openTabButton) { + throw new Error('Unable to find the open tab button.'); + } + + return await openTabButton.click(); + } + + /** + * Clicks a tab button with a `tabHeading` matching the input. + */ + public async clickTabButton(tabHeading: string): Promise { + const tabButton = await this.getTabButtonHarness(tabHeading); + return await tabButton.click(); + } + + /** + * Gets the active tab button harness. + */ + public async getActiveTabButton(): Promise { + const tabButtonHarnesses = await this.getTabButtonHarnesses(); + + for (const harness of tabButtonHarnesses) { + if (await harness.isActive()) { + return harness; + } + } + + /* istanbul ignore next */ + return null; + } + + /** + * Gets the tabset aria-label value. + */ + public async getAriaLabel(): Promise { + return await (await this.#getTabset()).getAttribute('aria-label'); + } + + /** + * Gets the tabset aria-labelledby value. + */ + public async getAriaLabelledBy(): Promise { + return await (await this.#getTabset()).getAttribute('aria-labelledby'); + } + + /** + * Gets the tabset's display mode. + */ + public async getMode(): Promise { + if (await (await this.#getTabset()).hasClass('sky-tabset-mode-tabs')) { + return 'tabs'; + } + return 'dropdown'; + } + + /** + * Gets a tab button harness with the given `tabHeading` + */ + public async getTabButtonHarness( + tabHeading: string, + ): Promise { + if ((await this.getMode()) === 'dropdown') { + const menu = await this.#getDropdownMenu(); + return await menu.queryHarness( + SkyTabButtonHarness.with({ tabHeading: tabHeading }), + ); + } + return await this.locatorFor( + SkyTabButtonHarness.with({ tabHeading: tabHeading }), + )(); + } + + /** + * Gets an array of all tab button harnesses. + */ + public async getTabButtonHarnesses(): Promise { + if ((await this.getMode()) === 'dropdown') { + const menu = await this.#getDropdownMenu(); + return await menu.queryHarnesses(SkyTabButtonHarness); + } + return await this.#getTabButtons(); + } + + /** + * Gets a tab content harness for the tab with the given `tabHeading`. + */ + public async getTabContentHarness( + tabHeading: string, + ): Promise { + const tabButton = await this.getTabButtonHarness(tabHeading); + const id = await tabButton.getTabId(); + return await this.locatorFor(SkyTabContentHarness.with({ tabId: id }))(); + } + + async #getDropdownMenu(): Promise { + const dropdown = await this.#getTabDropdown(); + if (!(await dropdown?.isOpen())) { + throw new Error( + 'Cannot get tab button when tabs is in dropdown mode and is closed.', + ); + } + return await dropdown.getDropdownMenu(); + } +} diff --git a/libs/components/tabs/testing/src/public-api.ts b/libs/components/tabs/testing/src/public-api.ts index bc42224f92..7c5bde1790 100644 --- a/libs/components/tabs/testing/src/public-api.ts +++ b/libs/components/tabs/testing/src/public-api.ts @@ -1,2 +1,8 @@ export { SkyTabsetFixtureTab } from './legacy/tabs/tab-fixture-tab'; export { SkyTabsetFixture } from './legacy/tabs/tabset-fixture'; + +export { SkyTabsetHarness } from './modules/tabs/tabset-harness'; +export { SkyTabsetHarnessFilters } from './modules/tabs/tabset-harness-filters'; +export { SkyTabButtonHarness } from './modules/tabs/tab-button-harness'; +export { SkyTabButtonHarnessFilters } from './modules/tabs/tab-button-harness-filters'; +export { SkyTabContentHarness } from './modules/tabs/tab-content-harness';