Skip to content

Commit

Permalink
feat(material/tabs/testing): polish harness API (#17417)
Browse files Browse the repository at this point in the history
* feat(material/tabs/testing): polish harness API

* replace `getSelectorForContent` with `getHarnessLoaderForContent`

* update @angular/cdk/testing API guards

* address comments

* add `getTextContent` to `MatTabHarness`
  • Loading branch information
mmalerba authored Oct 17, 2019
1 parent bb9a3a8 commit 05600a2
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 33 deletions.
23 changes: 23 additions & 0 deletions src/cdk/testing/component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,29 @@ export interface LocatorFactory {
locatorForAll<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;

/**
* Gets a `HarnessLoader` instance for an element under the root of this `LocatorFactory`.
* @param selector The selector for the root element.
* @return A `HarnessLoader` rooted at the first element matching the given selector.
* @throws If no matching element is found for the given selector.
*/
harnessLoaderFor(selector: string): Promise<HarnessLoader>;

/**
* Gets a `HarnessLoader` instance for an element under the root of this `LocatorFactory`
* @param selector The selector for the root element.
* @return A `HarnessLoader` rooted at the first element matching the given selector, or null if
* no matching element is found.
*/
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;

/**
* Gets a list of `HarnessLoader` instances, one for each matching element.
* @param selector The selector for the root element.
* @return A list of `HarnessLoader`, one rooted at each element matching the given selector.
*/
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;

/**
* Flushes change detection and async tasks.
* In most cases it should not be necessary to call this manually. However, there may be some edge
Expand Down
17 changes: 17 additions & 0 deletions src/cdk/testing/harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
};
}

// Implemented as part of the `LocatorFactory` interface.
async harnessLoaderFor(selector: string): Promise<HarnessLoader> {
return this.createEnvironment(await this._assertElementFound(selector));
}

// Implemented as part of the `LocatorFactory` interface.
async harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null> {
const elements = await this.getAllRawElements(selector);
return elements[0] ? this.createEnvironment(elements[0]) : null;
}

// Implemented as part of the `LocatorFactory` interface.
async harnessLoaderForAll(selector: string): Promise<HarnessLoader[]> {
const elements = await this.getAllRawElements(selector);
return elements.map(element => this.createEnvironment(element));
}

// Implemented as part of the `HarnessLoader` interface.
getHarness<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T> {
Expand Down
1 change: 1 addition & 0 deletions src/material/tabs/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ng_test_library(
srcs = ["shared.spec.ts"],
deps = [
":testing",
"//src/cdk/private/testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/tabs",
Expand Down
50 changes: 38 additions & 12 deletions src/material/tabs/testing/shared.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {HarnessLoader} from '@angular/cdk/testing';
import {expectAsyncError} from '@angular/cdk/private/testing';
import {ComponentHarness, HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
Expand Down Expand Up @@ -51,6 +52,26 @@ export function runHarnessTests(
expect(tabs.length).toBe(3);
});

it('should be able to get filtered tabs', async () => {
const tabGroup = await loader.getHarness(tabGroupHarness);
const tabs = await tabGroup.getTabs({label: 'Third'});
expect(tabs.length).toBe(1);
expect(await tabs[0].getLabel()).toBe('Third');
});

it('should be able to select tab from tab-group', async () => {
const tabGroup = await loader.getHarness(tabGroupHarness);
expect(await (await tabGroup.getSelectedTab()).getLabel()).toBe('First');
await tabGroup.selectTab({label: 'Second'});
expect(await (await tabGroup.getSelectedTab()).getLabel()).toBe('Second');
});

it('should throw error when attempting to select invalid tab', async () => {
const tabGroup = await loader.getHarness(tabGroupHarness);
await expectAsyncError(() => tabGroup.selectTab({label: 'Fake'}),
/Error: Cannot find mat-tab matching filter {"label":"Fake"}/);
});

it('should be able to get label of tabs', async () => {
const tabGroup = await loader.getHarness(tabGroupHarness);
const tabs = await tabGroup.getTabs();
Expand All @@ -75,16 +96,13 @@ export function runHarnessTests(
expect(await tabs[2].getAriaLabelledby()).toBe('tabLabelId');
});

it('should be able to get content element of active tab', async () => {
it('should be able to get harness loader for 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');
expect(await tabs[0].getTextContent()).toBe('Content 1');
const tabContentLoader = await tabs[0].getHarnessLoaderForContent();
const tabContentHarness = await tabContentLoader.getHarness(TestTabContentHarness);
expect(await (await tabContentHarness.host()).text()).toBe('Content 1');
});

it('should be able to get disabled state of tab', async () => {
Expand Down Expand Up @@ -136,15 +154,23 @@ export function runHarnessTests(
@Component({
template: `
<mat-tab-group>
<mat-tab label="First" aria-label="First tab">Content 1</mat-tab>
<mat-tab label="Second" aria-label="Second tab">Content 2</mat-tab>
<mat-tab label="First" aria-label="First tab">
<span class="test-tab-content">Content 1</span>
</mat-tab>
<mat-tab label="Second" aria-label="Second tab">
<span class="test-tab-content">Content 2</span>
</mat-tab>
<mat-tab label="Third" aria-labelledby="tabLabelId" [disabled]="isDisabled">
<ng-template matTabLabel>Third</ng-template>
Content 3
<span class="test-tab-content">Content 3</span>
</mat-tab>
</mat-tab-group>
`
})
class TabGroupHarnessTest {
isDisabled = false;
}

class TestTabContentHarness extends ComponentHarness {
static hostSelector = '.test-tab-content';
}
17 changes: 12 additions & 5 deletions src/material/tabs/testing/tab-group-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
import {TabGroupHarnessFilters} from './tab-harness-filters';
import {TabGroupHarnessFilters, TabHarnessFilters} from './tab-harness-filters';
import {MatTabHarness} from './tab-harness';

/**
Expand All @@ -34,11 +34,9 @@ export class MatTabGroupHarness extends ComponentHarness {
});
}

private _tabs = this.locatorForAll(MatTabHarness);

/** Gets all tabs of the tab group. */
async getTabs(): Promise<MatTabHarness[]> {
return this._tabs();
async getTabs(filter: TabHarnessFilters = {}): Promise<MatTabHarness[]> {
return this.locatorForAll(MatTabHarness.with(filter))();
}

/** Gets the selected tab of the tab group. */
Expand All @@ -52,4 +50,13 @@ export class MatTabGroupHarness extends ComponentHarness {
}
throw new Error('No selected tab could be found.');
}

/** Selects a tab in this tab group. */
async selectTab(filter: TabHarnessFilters = {}): Promise<void> {
const tabs = await this.getTabs(filter);
if (!tabs.length) {
throw Error(`Cannot find mat-tab matching filter ${JSON.stringify(filter)}`);
}
await tabs[0].select();
}
}
4 changes: 3 additions & 1 deletion src/material/tabs/testing/tab-harness-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
*/
import {BaseHarnessFilters} from '@angular/cdk/testing';

export interface TabHarnessFilters extends BaseHarnessFilters {}
export interface TabHarnessFilters extends BaseHarnessFilters {
label?: string | RegExp;
}

export interface TabGroupHarnessFilters extends BaseHarnessFilters {
selectedTabLabel?: string | RegExp;
Expand Down
33 changes: 20 additions & 13 deletions src/material/tabs/testing/tab-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentHarness, HarnessPredicate, TestElement} from '@angular/cdk/testing';
import {ComponentHarness, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
import {TabHarnessFilters} from './tab-harness-filters';

/**
Expand All @@ -20,11 +20,11 @@ export class MatTabHarness extends ComponentHarness {
* Gets a `HarnessPredicate` that can be used to search for a tab with specific attributes.
*/
static with(options: TabHarnessFilters = {}): HarnessPredicate<MatTabHarness> {
return new HarnessPredicate(MatTabHarness, options);
return new HarnessPredicate(MatTabHarness, options)
.addOption('label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label));
}

private _rootLocatorFactory = this.documentRootLocatorFactory();

/** Gets the label of the tab. */
async getLabel(): Promise<string> {
return (await this.host()).text();
Expand All @@ -40,15 +40,6 @@ export class MatTabHarness extends ComponentHarness {
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<TestElement> {
return this._rootLocatorFactory.locatorFor(`#${await this._getContentId()}`)();
}

/** Whether the tab is selected. */
async isSelected(): Promise<boolean> {
const hostEl = await this.host();
Expand All @@ -69,6 +60,22 @@ export class MatTabHarness extends ComponentHarness {
await (await this.host()).click();
}

/** Gets the text content of the tab. */
async getTextContent(): Promise<string> {
const contentId = await this._getContentId();
const contentEl = await this.documentRootLocatorFactory().locatorFor(`#${contentId}`)();
return contentEl.text();
}

/**
* Gets a `HarnessLoader` that can be used to load harnesses for components within the tab's
* content area.
*/
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
const contentId = await this._getContentId();
return this.documentRootLocatorFactory().harnessLoaderFor(`#${contentId}`);
}

/** Gets the element id for the content of the current tab. */
private async _getContentId(): Promise<string> {
const hostEl = await this.host();
Expand Down
10 changes: 8 additions & 2 deletions tools/public_api_guard/cdk/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export declare abstract class HarnessEnvironment<E> implements HarnessLoader, Lo
getChildLoader(selector: string): Promise<HarnessLoader>;
protected abstract getDocumentRoot(): E;
getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T>;
harnessLoaderFor(selector: string): Promise<HarnessLoader>;
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;
locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
Expand Down Expand Up @@ -96,10 +99,13 @@ export interface LocatorFactory {
rootElement: TestElement;
documentRootLocatorFactory(): LocatorFactory;
forceStabilize(): Promise<void>;
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
harnessLoaderFor(selector: string): Promise<HarnessLoader>;
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;
locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;
locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;
locatorForOptional(selector: string): AsyncFactoryFn<TestElement | null>;
locatorForOptional<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T | null>;
}
Expand Down

0 comments on commit 05600a2

Please sign in to comment.