diff --git a/src/material-experimental/mdc-tabs/harness/BUILD.bazel b/src/material-experimental/mdc-tabs/harness/BUILD.bazel new file mode 100644 index 000000000000..3d181b78e4ce --- /dev/null +++ b/src/material-experimental/mdc-tabs/harness/BUILD.bazel @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library") + +ts_library( + name = "harness", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src/cdk-experimental/testing", + "//src/cdk/coercion", + ], +) + +ng_test_library( + name = "harness_tests", + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":harness", + "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/testbed", + "//src/material/tabs", + "@npm//@angular/platform-browser", + ], +) + +ng_web_test_suite( + name = "tests", + deps = [":harness_tests"], +) diff --git a/src/material-experimental/mdc-tabs/harness/tab-group-harness-filters.ts b/src/material-experimental/mdc-tabs/harness/tab-group-harness-filters.ts new file mode 100644 index 000000000000..c286c45e7dd4 --- /dev/null +++ b/src/material-experimental/mdc-tabs/harness/tab-group-harness-filters.ts @@ -0,0 +1,9 @@ +/** + * @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.io/license + */ + +export type TabGroupHarnessFilters = {}; diff --git a/src/material-experimental/mdc-tabs/harness/tab-group-harness.spec.ts b/src/material-experimental/mdc-tabs/harness/tab-group-harness.spec.ts new file mode 100644 index 000000000000..191e5a1f1f5c --- /dev/null +++ b/src/material-experimental/mdc-tabs/harness/tab-group-harness.spec.ts @@ -0,0 +1,148 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatTabsModule} from '@angular/material/tabs'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {MatTabGroupHarness} from './tab-group-harness'; + +let fixture: ComponentFixture; +let loader: HarnessLoader; +let tabGroupHarness: typeof MatTabGroupHarness; + +describe('MatTabGroupHarness', () => { + describe('non-MDC-based', () => { + beforeEach(async () => { + await TestBed + .configureTestingModule({ + imports: [MatTabsModule, NoopAnimationsModule], + declarations: [TabGroupHarnessTest], + }) + .compileComponents(); + + fixture = TestBed.createComponent(TabGroupHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + tabGroupHarness = MatTabGroupHarness; + }); + + runTests(); + }); + + describe( + 'MDC-based', + () => { + // TODO: run tests for MDC based tab-group once implemented. + }); +}); + +/** Shared tests to run on both the original and MDC-based tab-group's. */ +function runTests() { + it('should load harness for tab-group', async () => { + const tabGroups = await loader.getAllHarnesses(tabGroupHarness); + expect(tabGroups.length).toBe(1); + }); + + it('should be able to get tabs of tab-group', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(tabs.length).toBe(3); + }); + + it('should be able to get label of tabs', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await tabs[0].getLabel()).toBe('First'); + expect(await tabs[1].getLabel()).toBe('Second'); + expect(await tabs[2].getLabel()).toBe('Third'); + }); + + it('should be able to get aria-label of tabs', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await tabs[0].getAriaLabel()).toBe('First tab'); + expect(await tabs[1].getAriaLabel()).toBe('Second tab'); + expect(await tabs[2].getAriaLabel()).toBe(null); + }); + + it('should be able to get aria-labelledby of tabs', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await tabs[0].getAriaLabelledby()).toBe(null); + expect(await tabs[1].getAriaLabelledby()).toBe(null); + expect(await tabs[2].getAriaLabelledby()).toBe('tabLabelId'); + }); + + it('should be able to get content element of active tab', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await (await tabs[0].getContentElement()).text()).toBe('Content 1'); + }); + + it('should be able to get content element of active tab', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await (await tabs[0].getContentElement()).text()).toBe('Content 1'); + }); + + it('should be able to get disabled state of tab', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await tabs[0].isDisabled()).toBe(false); + expect(await tabs[1].isDisabled()).toBe(false); + expect(await tabs[2].isDisabled()).toBe(false); + + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + + expect(await tabs[0].isDisabled()).toBe(false); + expect(await tabs[1].isDisabled()).toBe(false); + expect(await tabs[2].isDisabled()).toBe(true); + }); + + it('should be able to select specific tab', async () => { + const tabGroup = await loader.getHarness(tabGroupHarness); + const tabs = await tabGroup.getTabs(); + expect(await tabs[0].isSelected()).toBe(true); + expect(await tabs[1].isSelected()).toBe(false); + expect(await tabs[2].isSelected()).toBe(false); + + await tabs[1].select(); + expect(await tabs[0].isSelected()).toBe(false); + expect(await tabs[1].isSelected()).toBe(true); + expect(await tabs[2].isSelected()).toBe(false); + + // Should not be able to select third tab if disabled. + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + + await tabs[2].select(); + expect(await tabs[0].isSelected()).toBe(false); + expect(await tabs[1].isSelected()).toBe(true); + expect(await tabs[2].isSelected()).toBe(false); + + // Should be able to select third tab if not disabled. + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + await tabs[2].select(); + expect(await tabs[0].isSelected()).toBe(false); + expect(await tabs[1].isSelected()).toBe(false); + expect(await tabs[2].isSelected()).toBe(true); + }); +} + +@Component({ + template: ` + + Content 1 + Content 2 + + Third + Content 3 + + + ` +}) +class TabGroupHarnessTest { + isDisabled = false; +} diff --git a/src/material-experimental/mdc-tabs/harness/tab-group-harness.ts b/src/material-experimental/mdc-tabs/harness/tab-group-harness.ts new file mode 100644 index 000000000000..005b7878e2ae --- /dev/null +++ b/src/material-experimental/mdc-tabs/harness/tab-group-harness.ts @@ -0,0 +1,50 @@ +/** + * @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.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing'; +import {TabGroupHarnessFilters} from './tab-group-harness-filters'; +import {MatTabHarness} from './tab-harness'; + +/** + * Harness for interacting with a standard mat-tab-group in tests. + * @dynamic + */ +export class MatTabGroupHarness extends ComponentHarness { + static hostSelector = '.mat-tab-group'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a radio-button with + * specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: TabGroupHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatTabGroupHarness); + } + + private _tabs = this.locatorForAll(MatTabHarness); + + /** Gets all tabs of the tab group. */ + async getTabs(): Promise { + return this._tabs(); + } + + /** Gets the selected tab of the tab group. */ + async getSelectedTab(): Promise { + const tabs = await this.getTabs(); + const isSelected = await Promise.all(tabs.map(t => t.isSelected())); + for (let i = 0; i < tabs.length; i++) { + if (isSelected[i]) { + return tabs[i]; + } + } + throw new Error('No selected tab could be found.'); + } +} + + diff --git a/src/material-experimental/mdc-tabs/harness/tab-harness.ts b/src/material-experimental/mdc-tabs/harness/tab-harness.ts new file mode 100644 index 000000000000..15d8118862e1 --- /dev/null +++ b/src/material-experimental/mdc-tabs/harness/tab-harness.ts @@ -0,0 +1,70 @@ +/** + * @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.io/license + */ + +import {ComponentHarness, TestElement} from '@angular/cdk-experimental/testing'; + +/** + * Harness for interacting with a standard Angular Material tab-label in tests. + * @dynamic + */ +export class MatTabHarness extends ComponentHarness { + static hostSelector = '.mat-tab-label'; + + private _rootLocatorFactory = this.documentRootLocatorFactory(); + + /** Gets the label of the tab. */ + async getLabel(): Promise { + return (await this.host()).text(); + } + + /** Gets the aria label of the tab. */ + async getAriaLabel(): Promise { + return (await this.host()).getAttribute('aria-label'); + } + + /** Gets the value of the "aria-labelledby" attribute. */ + async getAriaLabelledby(): Promise { + return (await this.host()).getAttribute('aria-labelledby'); + } + + /** + * Gets the content element of the given tab. Note that the element will be empty + * until the tab is selected. This is an implementation detail of the tab-group + * in order to avoid rendering of non-active tabs. + */ + async getContentElement(): Promise { + return this._rootLocatorFactory.locatorFor(`#${await this._getContentId()}`)(); + } + + /** Whether the tab is selected. */ + async isSelected(): Promise { + const hostEl = await this.host(); + return (await hostEl.getAttribute('aria-selected')) === 'true'; + } + + /** Whether the tab is disabled. */ + async isDisabled(): Promise { + const hostEl = await this.host(); + return (await hostEl.getAttribute('aria-disabled')) === 'true'; + } + + /** + * Selects the given tab by clicking on the label. Tab cannot be + * selected if disabled. + */ + async select(): Promise { + await (await this.host()).click(); + } + + /** Gets the element id for the content of the current tab. */ + private async _getContentId(): Promise { + const hostEl = await this.host(); + // Tabs never have an empty "aria-controls" attribute. + return (await hostEl.getAttribute('aria-controls'))!; + } +}