Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(material/tabs/testing): polish harness API #17417

Merged
merged 5 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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