Skip to content

feat(material/tree): add test harness for MatTree #20323

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

Merged
merged 4 commits into from
Sep 23, 2020
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
1 change: 1 addition & 0 deletions src/material/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ entryPoints = [
"tooltip",
"tooltip/testing",
"tree",
"tree/testing",
"form-field/testing",
"form-field/testing/control",
"input/testing",
Expand Down
51 changes: 51 additions & 0 deletions src/material/tree/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/material/tree/testing",
deps = [
"//src/cdk/coercion",
"//src/cdk/testing",
],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)

ng_test_library(
name = "harness_tests_lib",
srcs = ["shared.spec.ts"],
deps = [
":testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/cdk/tree",
"//src/material/tree",
],
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(
["**/*.spec.ts"],
exclude = ["shared.spec.ts"],
),
deps = [
":harness_tests_lib",
":testing",
"//src/material/tree",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
9 changes: 9 additions & 0 deletions src/material/tree/testing/index.ts
Original file line number Diff line number Diff line change
@@ -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 * from './public-api';
91 changes: 91 additions & 0 deletions src/material/tree/testing/node-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @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,
ComponentHarnessConstructor,
HarnessPredicate
} from '@angular/cdk/testing';
import {TreeNodeHarnessFilters} from './tree-harness-filters';
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';

/** Harness for interacting with a standard Angular Material tree node. */
export class MatTreeNodeHarness extends ComponentHarness {
/** The selector of the host element of a `MatTreeNode` instance. */
static hostSelector = '.mat-tree-node, .mat-nested-tree-node';

_toggle = this.locatorForOptional('[matTreeNodeToggle]');

/**
* Gets a `HarnessPredicate` that can be used to search for a tree node with specific attributes.
* @param options Options for narrowing the search
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: TreeNodeHarnessFilters = {}): HarnessPredicate<MatTreeNodeHarness> {
return getNodePredicate(MatTreeNodeHarness, options);
}

/** Whether the tree node is expanded. */
async isExpanded(): Promise<boolean> {
return coerceBooleanProperty(await (await this.host()).getAttribute('aria-expanded'));
}

/** Whether the tree node is disabled. */
async isDisabled(): Promise<boolean> {
return coerceBooleanProperty(await (await this.host()).getProperty('aria-disabled'));
}

/** Gets the level of the tree node. Note that this gets the aria-level and is 1 indexed. */
async getLevel(): Promise<number> {
return coerceNumberProperty(await (await this.host()).getAttribute('aria-level'));
}

/** Gets the tree node's text. */
async getText(): Promise<string> {
return (await this.host()).text({exclude: '.mat-tree-node, .mat-nested-tree-node, button'});
}

/** Toggles node between expanded/collapsed. Only works when node is not disabled. */
async toggle(): Promise<void> {
const toggle = await this._toggle();
if (toggle) {
return toggle.click();
}
}

/** Expands the node if it is collapsed. Only works when node is not disabled. */
async expand(): Promise<void> {
if (!(await this.isExpanded())) {
await this.toggle();
}
}

/** Collapses the node if it is expanded. Only works when node is not disabled. */
async collapse(): Promise<void> {
if (await this.isExpanded()) {
await this.toggle();
}
}
}

function getNodePredicate<T extends MatTreeNodeHarness>(
type: ComponentHarnessConstructor<T>,
options: TreeNodeHarnessFilters): HarnessPredicate<T> {
return new HarnessPredicate(type, options)
.addOption('text', options.text,
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
.addOption(
'disabled', options.disabled,
async (harness, disabled) => (await harness.isDisabled()) === disabled)
.addOption(
'expanded', options.expanded,
async (harness, expanded) => (await harness.isExpanded()) === expanded)
.addOption(
'level', options.level,
async (harness, level) => (await harness.getLevel()) === level);
}
11 changes: 11 additions & 0 deletions src/material/tree/testing/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @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 * from './node-harness';
export * from './tree-harness';
export * from './tree-harness-filters';
215 changes: 215 additions & 0 deletions src/material/tree/testing/shared.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {Component} from '@angular/core';
import {FlatTreeControl, NestedTreeControl} from '@angular/cdk/tree';
import {
MatTreeFlatDataSource,
MatTreeFlattener,
MatTreeModule,
MatTreeNestedDataSource
} from '@angular/material/tree';
import {MatTreeHarness} from '@angular/material/tree/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';

/** Shared tests to run on both the original and MDC-based trees. */
export function runHarnessTests(
treeModule: typeof MatTreeModule,
treeHarness: typeof MatTreeHarness) {
let fixture: ComponentFixture<TreeHarnessTest>;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [treeModule],
declarations: [TreeHarnessTest],
}).compileComponents();

fixture = TestBed.createComponent(TreeHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
});

it('should load harness with 2 tress', async () => {
const trees = await loader.getAllHarnesses(treeHarness);

expect(trees.length).toBe(2);
});

it('should get correct number of children and descendants', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const flatTree = trees[0];
const nestedTree = trees[1];
const flatTreeDescendants = await flatTree.getNodes();
const nestedDescendants = await nestedTree.getNodes();

// flat nodes are not rendered until expanded
expect(flatTreeDescendants.length).toBe(2);

await flatTreeDescendants[0].expand();

expect((await flatTree.getNodes()).length).toBe(5);

expect(nestedDescendants.length).toBe(8);
});

it('should correctly get correct node with text (flat tree)', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const flatTree = trees[0];
const flatTreeNodes = await flatTree.getNodes({text: /Flat Group/});
expect(flatTreeNodes.length).toBe(2);
const secondGroup = flatTreeNodes[0];

expect(await secondGroup.getText()).toBe('Flat Group 1');
expect(await secondGroup.getLevel()).toBe(1);
expect(await secondGroup.isDisabled()).toBe(false);
expect(await secondGroup.isExpanded()).toBe(false);
});

it('should correctly get correct node with text (nested tree)', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const nestedTree = trees[1];
const nestedTreeNodes = await nestedTree.getNodes({text: /2./});
expect(nestedTreeNodes.length).toBe(3);
const thirdGroup = nestedTreeNodes[1];

expect(await thirdGroup.getText()).toBe('Nested Leaf 2.1.1');
expect(await thirdGroup.getLevel()).toBe(3);
expect(await thirdGroup.isDisabled()).toBe(false);
expect(await thirdGroup.isExpanded()).toBe(false);
});

it('should toggle expansion', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const nestedTree = trees[1];
const nestedTreeNodes = await nestedTree.getNodes();
const firstGroup = nestedTreeNodes[0];

expect(await firstGroup.isExpanded()).toBe(false);
await firstGroup.expand();
expect(await firstGroup.isExpanded()).toBe(true);
await firstGroup.expand();
// no-op if already expanded
expect(await firstGroup.isExpanded()).toBe(true);
await firstGroup.collapse();
expect(await firstGroup.isExpanded()).toBe(false);
await firstGroup.collapse();
// no-op if already collapsed
expect(await firstGroup.isExpanded()).toBe(false);
});
}

interface FoodNode {
name: string;
children?: FoodNode[];
}

const FLAT_TREE_DATA: FoodNode[] = [
{
name: 'Flat Group 1',
children: [
{name: 'Flat Leaf 1.1'},
{name: 'Flat Leaf 1.2'},
{name: 'Flat Leaf 1.3'},
]
}, {
name: 'Flat Group 2',
children: [
{
name: 'Flat Group 2.1',
children: [
{name: 'Flat Leaf 2.1.1'},
{name: 'Flat Leaf 2.1.2'},
{name: 'Flat Leaf 2.1.3'},
]
}
]
},
];

const NESTED_TREE_DATA: FoodNode[] = [
{
name: 'Nested Group 1',
children: [
{name: 'Nested Leaf 1.1'},
{name: 'Nested Leaf 1.2'},
{name: 'Nested Leaf 1.3'},
]
}, {
name: 'Nested Group 2',
children: [
{
name: 'Nested Group 2.1',
children: [
{name: 'Nested Leaf 2.1.1'},
{name: 'Nested Leaf 2.1.2'},
]
},
]
},
];

interface ExampleFlatNode {
expandable: boolean;
name: string;
level: number;
}

@Component({
template: `
<mat-tree [dataSource]="flatTreeDataSource" [treeControl]="flatTreeControl">
<!-- This is the tree node template for leaf nodes -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: flatTreeHasChild" matTreeNodePadding>
<button matTreeNodeToggle>
Toggle
</button>
{{node.name}}
</mat-tree-node>
</mat-tree>
<mat-tree [dataSource]="nestedTreeDataSource" [treeControl]="nestedTreeControl">
<!-- This is the tree node template for leaf nodes -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-nested-tree-node *matTreeNodeDef="let node; when: nestedTreeHasChild">
<button matTreeNodeToggle>
Toggle
</button>
{{node.name}}
<ul [class.example-tree-invisible]="!nestedTreeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</mat-nested-tree-node>
</mat-tree>
`
})
class TreeHarnessTest {
private _transformer = (node: FoodNode, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
name: node.name,
level: level,
};
}

treeFlattener = new MatTreeFlattener(
this._transformer, node => node.level, node => node.expandable, node => node.children);
flatTreeControl = new FlatTreeControl<ExampleFlatNode>(
node => node.level, node => node.expandable);
flatTreeDataSource = new MatTreeFlatDataSource(this.flatTreeControl, this.treeFlattener);
nestedTreeControl = new NestedTreeControl<FoodNode>(node => node.children);
nestedTreeDataSource = new MatTreeNestedDataSource<FoodNode>();

constructor() {
this.flatTreeDataSource.data = FLAT_TREE_DATA;
this.nestedTreeDataSource.data = NESTED_TREE_DATA;
}

flatTreeHasChild = (_: number, node: ExampleFlatNode) => node.expandable;

nestedTreeHasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
}
Loading