Skip to content

Commit 5916800

Browse files
authored
feat(material/tree): add test harness for MatTree (#20323)
* tree harness basic implementation - flat tree nodes throw error for getChildren/getDescendants * only implement getNodes for tree for now * fix imports and add entrypoint to config.bzl * clean up duplicate host:{} and @HostBinding
1 parent 993767f commit 5916800

File tree

10 files changed

+472
-0
lines changed

10 files changed

+472
-0
lines changed

src/material/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ entryPoints = [
6565
"tooltip",
6666
"tooltip/testing",
6767
"tree",
68+
"tree/testing",
6869
"form-field/testing",
6970
"form-field/testing/control",
7071
"input/testing",

src/material/tree/testing/BUILD.bazel

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material/tree/testing",
12+
deps = [
13+
"//src/cdk/coercion",
14+
"//src/cdk/testing",
15+
],
16+
)
17+
18+
filegroup(
19+
name = "source-files",
20+
srcs = glob(["**/*.ts"]),
21+
)
22+
23+
ng_test_library(
24+
name = "harness_tests_lib",
25+
srcs = ["shared.spec.ts"],
26+
deps = [
27+
":testing",
28+
"//src/cdk/testing",
29+
"//src/cdk/testing/testbed",
30+
"//src/cdk/tree",
31+
"//src/material/tree",
32+
],
33+
)
34+
35+
ng_test_library(
36+
name = "unit_tests_lib",
37+
srcs = glob(
38+
["**/*.spec.ts"],
39+
exclude = ["shared.spec.ts"],
40+
),
41+
deps = [
42+
":harness_tests_lib",
43+
":testing",
44+
"//src/material/tree",
45+
],
46+
)
47+
48+
ng_web_test_suite(
49+
name = "unit_tests",
50+
deps = [":unit_tests_lib"],
51+
)

src/material/tree/testing/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
ComponentHarness,
11+
ComponentHarnessConstructor,
12+
HarnessPredicate
13+
} from '@angular/cdk/testing';
14+
import {TreeNodeHarnessFilters} from './tree-harness-filters';
15+
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
16+
17+
/** Harness for interacting with a standard Angular Material tree node. */
18+
export class MatTreeNodeHarness extends ComponentHarness {
19+
/** The selector of the host element of a `MatTreeNode` instance. */
20+
static hostSelector = '.mat-tree-node, .mat-nested-tree-node';
21+
22+
_toggle = this.locatorForOptional('[matTreeNodeToggle]');
23+
24+
/**
25+
* Gets a `HarnessPredicate` that can be used to search for a tree node with specific attributes.
26+
* @param options Options for narrowing the search
27+
* @return a `HarnessPredicate` configured with the given options.
28+
*/
29+
static with(options: TreeNodeHarnessFilters = {}): HarnessPredicate<MatTreeNodeHarness> {
30+
return getNodePredicate(MatTreeNodeHarness, options);
31+
}
32+
33+
/** Whether the tree node is expanded. */
34+
async isExpanded(): Promise<boolean> {
35+
return coerceBooleanProperty(await (await this.host()).getAttribute('aria-expanded'));
36+
}
37+
38+
/** Whether the tree node is disabled. */
39+
async isDisabled(): Promise<boolean> {
40+
return coerceBooleanProperty(await (await this.host()).getProperty('aria-disabled'));
41+
}
42+
43+
/** Gets the level of the tree node. Note that this gets the aria-level and is 1 indexed. */
44+
async getLevel(): Promise<number> {
45+
return coerceNumberProperty(await (await this.host()).getAttribute('aria-level'));
46+
}
47+
48+
/** Gets the tree node's text. */
49+
async getText(): Promise<string> {
50+
return (await this.host()).text({exclude: '.mat-tree-node, .mat-nested-tree-node, button'});
51+
}
52+
53+
/** Toggles node between expanded/collapsed. Only works when node is not disabled. */
54+
async toggle(): Promise<void> {
55+
const toggle = await this._toggle();
56+
if (toggle) {
57+
return toggle.click();
58+
}
59+
}
60+
61+
/** Expands the node if it is collapsed. Only works when node is not disabled. */
62+
async expand(): Promise<void> {
63+
if (!(await this.isExpanded())) {
64+
await this.toggle();
65+
}
66+
}
67+
68+
/** Collapses the node if it is expanded. Only works when node is not disabled. */
69+
async collapse(): Promise<void> {
70+
if (await this.isExpanded()) {
71+
await this.toggle();
72+
}
73+
}
74+
}
75+
76+
function getNodePredicate<T extends MatTreeNodeHarness>(
77+
type: ComponentHarnessConstructor<T>,
78+
options: TreeNodeHarnessFilters): HarnessPredicate<T> {
79+
return new HarnessPredicate(type, options)
80+
.addOption('text', options.text,
81+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
82+
.addOption(
83+
'disabled', options.disabled,
84+
async (harness, disabled) => (await harness.isDisabled()) === disabled)
85+
.addOption(
86+
'expanded', options.expanded,
87+
async (harness, expanded) => (await harness.isExpanded()) === expanded)
88+
.addOption(
89+
'level', options.level,
90+
async (harness, level) => (await harness.getLevel()) === level);
91+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './node-harness';
10+
export * from './tree-harness';
11+
export * from './tree-harness-filters';
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import {Component} from '@angular/core';
2+
import {FlatTreeControl, NestedTreeControl} from '@angular/cdk/tree';
3+
import {
4+
MatTreeFlatDataSource,
5+
MatTreeFlattener,
6+
MatTreeModule,
7+
MatTreeNestedDataSource
8+
} from '@angular/material/tree';
9+
import {MatTreeHarness} from '@angular/material/tree/testing';
10+
import {ComponentFixture, TestBed} from '@angular/core/testing';
11+
import {HarnessLoader} from '@angular/cdk/testing';
12+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
13+
14+
/** Shared tests to run on both the original and MDC-based trees. */
15+
export function runHarnessTests(
16+
treeModule: typeof MatTreeModule,
17+
treeHarness: typeof MatTreeHarness) {
18+
let fixture: ComponentFixture<TreeHarnessTest>;
19+
let loader: HarnessLoader;
20+
21+
beforeEach(async () => {
22+
await TestBed.configureTestingModule({
23+
imports: [treeModule],
24+
declarations: [TreeHarnessTest],
25+
}).compileComponents();
26+
27+
fixture = TestBed.createComponent(TreeHarnessTest);
28+
fixture.detectChanges();
29+
loader = TestbedHarnessEnvironment.loader(fixture);
30+
});
31+
32+
it('should load harness with 2 tress', async () => {
33+
const trees = await loader.getAllHarnesses(treeHarness);
34+
35+
expect(trees.length).toBe(2);
36+
});
37+
38+
it('should get correct number of children and descendants', async () => {
39+
const trees = await loader.getAllHarnesses(treeHarness);
40+
const flatTree = trees[0];
41+
const nestedTree = trees[1];
42+
const flatTreeDescendants = await flatTree.getNodes();
43+
const nestedDescendants = await nestedTree.getNodes();
44+
45+
// flat nodes are not rendered until expanded
46+
expect(flatTreeDescendants.length).toBe(2);
47+
48+
await flatTreeDescendants[0].expand();
49+
50+
expect((await flatTree.getNodes()).length).toBe(5);
51+
52+
expect(nestedDescendants.length).toBe(8);
53+
});
54+
55+
it('should correctly get correct node with text (flat tree)', async () => {
56+
const trees = await loader.getAllHarnesses(treeHarness);
57+
const flatTree = trees[0];
58+
const flatTreeNodes = await flatTree.getNodes({text: /Flat Group/});
59+
expect(flatTreeNodes.length).toBe(2);
60+
const secondGroup = flatTreeNodes[0];
61+
62+
expect(await secondGroup.getText()).toBe('Flat Group 1');
63+
expect(await secondGroup.getLevel()).toBe(1);
64+
expect(await secondGroup.isDisabled()).toBe(false);
65+
expect(await secondGroup.isExpanded()).toBe(false);
66+
});
67+
68+
it('should correctly get correct node with text (nested tree)', async () => {
69+
const trees = await loader.getAllHarnesses(treeHarness);
70+
const nestedTree = trees[1];
71+
const nestedTreeNodes = await nestedTree.getNodes({text: /2./});
72+
expect(nestedTreeNodes.length).toBe(3);
73+
const thirdGroup = nestedTreeNodes[1];
74+
75+
expect(await thirdGroup.getText()).toBe('Nested Leaf 2.1.1');
76+
expect(await thirdGroup.getLevel()).toBe(3);
77+
expect(await thirdGroup.isDisabled()).toBe(false);
78+
expect(await thirdGroup.isExpanded()).toBe(false);
79+
});
80+
81+
it('should toggle expansion', async () => {
82+
const trees = await loader.getAllHarnesses(treeHarness);
83+
const nestedTree = trees[1];
84+
const nestedTreeNodes = await nestedTree.getNodes();
85+
const firstGroup = nestedTreeNodes[0];
86+
87+
expect(await firstGroup.isExpanded()).toBe(false);
88+
await firstGroup.expand();
89+
expect(await firstGroup.isExpanded()).toBe(true);
90+
await firstGroup.expand();
91+
// no-op if already expanded
92+
expect(await firstGroup.isExpanded()).toBe(true);
93+
await firstGroup.collapse();
94+
expect(await firstGroup.isExpanded()).toBe(false);
95+
await firstGroup.collapse();
96+
// no-op if already collapsed
97+
expect(await firstGroup.isExpanded()).toBe(false);
98+
});
99+
}
100+
101+
interface FoodNode {
102+
name: string;
103+
children?: FoodNode[];
104+
}
105+
106+
const FLAT_TREE_DATA: FoodNode[] = [
107+
{
108+
name: 'Flat Group 1',
109+
children: [
110+
{name: 'Flat Leaf 1.1'},
111+
{name: 'Flat Leaf 1.2'},
112+
{name: 'Flat Leaf 1.3'},
113+
]
114+
}, {
115+
name: 'Flat Group 2',
116+
children: [
117+
{
118+
name: 'Flat Group 2.1',
119+
children: [
120+
{name: 'Flat Leaf 2.1.1'},
121+
{name: 'Flat Leaf 2.1.2'},
122+
{name: 'Flat Leaf 2.1.3'},
123+
]
124+
}
125+
]
126+
},
127+
];
128+
129+
const NESTED_TREE_DATA: FoodNode[] = [
130+
{
131+
name: 'Nested Group 1',
132+
children: [
133+
{name: 'Nested Leaf 1.1'},
134+
{name: 'Nested Leaf 1.2'},
135+
{name: 'Nested Leaf 1.3'},
136+
]
137+
}, {
138+
name: 'Nested Group 2',
139+
children: [
140+
{
141+
name: 'Nested Group 2.1',
142+
children: [
143+
{name: 'Nested Leaf 2.1.1'},
144+
{name: 'Nested Leaf 2.1.2'},
145+
]
146+
},
147+
]
148+
},
149+
];
150+
151+
interface ExampleFlatNode {
152+
expandable: boolean;
153+
name: string;
154+
level: number;
155+
}
156+
157+
@Component({
158+
template: `
159+
<mat-tree [dataSource]="flatTreeDataSource" [treeControl]="flatTreeControl">
160+
<!-- This is the tree node template for leaf nodes -->
161+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
162+
{{node.name}}
163+
</mat-tree-node>
164+
<!-- This is the tree node template for expandable nodes -->
165+
<mat-tree-node *matTreeNodeDef="let node;when: flatTreeHasChild" matTreeNodePadding>
166+
<button matTreeNodeToggle>
167+
Toggle
168+
</button>
169+
{{node.name}}
170+
</mat-tree-node>
171+
</mat-tree>
172+
<mat-tree [dataSource]="nestedTreeDataSource" [treeControl]="nestedTreeControl">
173+
<!-- This is the tree node template for leaf nodes -->
174+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
175+
{{node.name}}
176+
</mat-tree-node>
177+
<!-- This is the tree node template for expandable nodes -->
178+
<mat-nested-tree-node *matTreeNodeDef="let node; when: nestedTreeHasChild">
179+
<button matTreeNodeToggle>
180+
Toggle
181+
</button>
182+
{{node.name}}
183+
<ul [class.example-tree-invisible]="!nestedTreeControl.isExpanded(node)">
184+
<ng-container matTreeNodeOutlet></ng-container>
185+
</ul>
186+
</mat-nested-tree-node>
187+
</mat-tree>
188+
`
189+
})
190+
class TreeHarnessTest {
191+
private _transformer = (node: FoodNode, level: number) => {
192+
return {
193+
expandable: !!node.children && node.children.length > 0,
194+
name: node.name,
195+
level: level,
196+
};
197+
}
198+
199+
treeFlattener = new MatTreeFlattener(
200+
this._transformer, node => node.level, node => node.expandable, node => node.children);
201+
flatTreeControl = new FlatTreeControl<ExampleFlatNode>(
202+
node => node.level, node => node.expandable);
203+
flatTreeDataSource = new MatTreeFlatDataSource(this.flatTreeControl, this.treeFlattener);
204+
nestedTreeControl = new NestedTreeControl<FoodNode>(node => node.children);
205+
nestedTreeDataSource = new MatTreeNestedDataSource<FoodNode>();
206+
207+
constructor() {
208+
this.flatTreeDataSource.data = FLAT_TREE_DATA;
209+
this.nestedTreeDataSource.data = NESTED_TREE_DATA;
210+
}
211+
212+
flatTreeHasChild = (_: number, node: ExampleFlatNode) => node.expandable;
213+
214+
nestedTreeHasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
215+
}

0 commit comments

Comments
 (0)