Skip to content

Commit 483c3e2

Browse files
authored
feat(material/tree): add getTreeStructure for tree harness (#20568)
* feat(material/tree): add getTreeStructure for tree harness * parallelize awaits, add comment * change to object representation
1 parent 70e9eb9 commit 483c3e2

File tree

3 files changed

+222
-9
lines changed

3 files changed

+222
-9
lines changed

src/material/tree/testing/shared.spec.ts

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,115 @@ export function runHarnessTests(
9696
// no-op if already collapsed
9797
expect(await firstGroup.isExpanded()).toBe(false);
9898
});
99+
100+
it ('should correctly get tree structure', async () => {
101+
const trees = await loader.getAllHarnesses(treeHarness);
102+
const flatTree = trees[0];
103+
104+
expect(await flatTree.getTreeStructure()).toEqual({
105+
children: [
106+
{text: 'Flat Group 1'},
107+
{text: 'Flat Group 2'}
108+
]
109+
});
110+
111+
const firstGroup = (await flatTree.getNodes({text: /Flat Group 1/}))[0];
112+
await firstGroup.expand();
113+
114+
expect(await flatTree.getTreeStructure()).toEqual({
115+
children: [
116+
{
117+
text: 'Flat Group 1',
118+
children: [
119+
{text: 'Flat Leaf 1.1'},
120+
{text: 'Flat Leaf 1.2'},
121+
{text: 'Flat Leaf 1.3'}
122+
]
123+
},
124+
{text: 'Flat Group 2'}
125+
]
126+
});
127+
128+
const secondGroup = (await flatTree.getNodes({text: /Flat Group 2/}))[0];
129+
await secondGroup.expand();
130+
131+
expect(await flatTree.getTreeStructure()).toEqual({
132+
children: [
133+
{
134+
text: 'Flat Group 1',
135+
children: [
136+
{text: 'Flat Leaf 1.1'},
137+
{text: 'Flat Leaf 1.2'},
138+
{text: 'Flat Leaf 1.3'}
139+
]
140+
},
141+
{
142+
text: 'Flat Group 2',
143+
children: [
144+
{text: 'Flat Group 2.1'},
145+
]
146+
}
147+
]
148+
});
149+
});
150+
151+
it('should correctly get tree structure', async () => {
152+
const trees = await loader.getAllHarnesses(treeHarness);
153+
const nestedTree = trees[1];
154+
expect(await nestedTree.getTreeStructure()).toEqual({
155+
children: [
156+
{text: 'Nested Group 1'},
157+
{text: 'Nested Group 2'}
158+
]
159+
});
160+
161+
const firstGroup = (await nestedTree.getNodes({text: /Nested Group 1/}))[0];
162+
await firstGroup.expand();
163+
expect(await nestedTree.getTreeStructure()).toEqual(
164+
{
165+
children: [
166+
{
167+
text: 'Nested Group 1',
168+
children: [
169+
{text: 'Nested Leaf 1.1'},
170+
{text: 'Nested Leaf 1.2'},
171+
{text: 'Nested Leaf 1.3'}
172+
]
173+
},
174+
{text: 'Nested Group 2'}
175+
]
176+
});
177+
178+
const secondGroup = (await nestedTree.getNodes({text: /Nested Group 2/}))[0];
179+
await secondGroup.expand();
180+
expect(await nestedTree.getTreeStructure()).toEqual(
181+
{
182+
children: [
183+
{
184+
text: 'Nested Group 1',
185+
children: [
186+
{text: 'Nested Leaf 1.1'},
187+
{text: 'Nested Leaf 1.2'},
188+
{text: 'Nested Leaf 1.3'}
189+
]
190+
},
191+
{
192+
text: 'Nested Group 2',
193+
children: [
194+
{text: 'Nested Group 2.1'},
195+
]
196+
}
197+
]
198+
});
199+
});
99200
}
100201

101-
interface FoodNode {
202+
interface Node {
102203
name: string;
103-
children?: FoodNode[];
204+
children?: Node[];
104205
}
105206

106-
const FLAT_TREE_DATA: FoodNode[] = [
207+
const FLAT_TREE_DATA: Node[] = [
107208
{
108209
name: 'Flat Group 1',
109210
children: [
@@ -126,7 +227,7 @@ const FLAT_TREE_DATA: FoodNode[] = [
126227
},
127228
];
128229

129-
const NESTED_TREE_DATA: FoodNode[] = [
230+
const NESTED_TREE_DATA: Node[] = [
130231
{
131232
name: 'Nested Group 1',
132233
children: [
@@ -188,7 +289,7 @@ interface ExampleFlatNode {
188289
`
189290
})
190291
class TreeHarnessTest {
191-
private _transformer = (node: FoodNode, level: number) => {
292+
private _transformer = (node: Node, level: number) => {
192293
return {
193294
expandable: !!node.children && node.children.length > 0,
194295
name: node.name,
@@ -201,8 +302,8 @@ class TreeHarnessTest {
201302
flatTreeControl = new FlatTreeControl<ExampleFlatNode>(
202303
node => node.level, node => node.expandable);
203304
flatTreeDataSource = new MatTreeFlatDataSource(this.flatTreeControl, this.treeFlattener);
204-
nestedTreeControl = new NestedTreeControl<FoodNode>(node => node.children);
205-
nestedTreeDataSource = new MatTreeNestedDataSource<FoodNode>();
305+
nestedTreeControl = new NestedTreeControl<Node>(node => node.children);
306+
nestedTreeDataSource = new MatTreeNestedDataSource<Node>();
206307

207308
constructor() {
208309
this.flatTreeDataSource.data = FLAT_TREE_DATA;
@@ -211,5 +312,5 @@ class TreeHarnessTest {
211312

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

214-
nestedTreeHasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
315+
nestedTreeHasChild = (_: number, node: Node) => !!node.children && node.children.length > 0;
215316
}

src/material/tree/testing/tree-harness.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
9+
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
1010
import {MatTreeNodeHarness} from './node-harness';
1111
import {TreeHarnessFilters, TreeNodeHarnessFilters} from './tree-harness-filters';
1212

13+
export type TextTree = {
14+
text?: string;
15+
children?: TextTree[];
16+
};
17+
1318
/** Harness for interacting with a standard mat-tree in tests. */
1419
export class MatTreeHarness extends ComponentHarness {
1520
/** The selector for the host element of a `MatTableHarness` instance. */
@@ -28,4 +33,105 @@ export class MatTreeHarness extends ComponentHarness {
2833
async getNodes(filter: TreeNodeHarnessFilters = {}): Promise<MatTreeNodeHarness[]> {
2934
return this.locatorForAll(MatTreeNodeHarness.with(filter))();
3035
}
36+
37+
/**
38+
* Gets an object representation for the visible tree structure
39+
* If a node is under an unexpanded node it will not be included.
40+
* Eg.
41+
* Tree (all nodes expanded):
42+
* `
43+
* <mat-tree>
44+
* <mat-tree-node>Node 1<mat-tree-node>
45+
* <mat-nested-tree-node>
46+
* Node 2
47+
* <mat-nested-tree-node>
48+
* Node 2.1
49+
* <mat-tree-node>
50+
* Node 2.1.1
51+
* <mat-tree-node>
52+
* <mat-nested-tree-node>
53+
* <mat-tree-node>
54+
* Node 2.2
55+
* <mat-tree-node>
56+
* <mat-nested-tree-node>
57+
* </mat-tree>`
58+
*
59+
* Tree structure:
60+
* {
61+
* children: [
62+
* {
63+
* text: 'Node 1',
64+
* children: [
65+
* {
66+
* text: 'Node 2',
67+
* children: [
68+
* {
69+
* text: 'Node 2.1',
70+
* children: [{text: 'Node 2.1.1'}]
71+
* },
72+
* {text: 'Node 2.2'}
73+
* ]
74+
* }
75+
* ]
76+
* }
77+
* ]
78+
* };
79+
*/
80+
async getTreeStructure(): Promise<TextTree> {
81+
const nodes = await this.getNodes();
82+
const nodeInformation = await parallel(() => nodes.map(node => {
83+
return Promise.all([node.getLevel(), node.getText(), node.isExpanded()]);
84+
}));
85+
return this._getTreeStructure(nodeInformation, 1, true);
86+
}
87+
88+
/**
89+
* Recursively collect the structured text of the tree nodes.
90+
* @param nodes A list of tree nodes
91+
* @param level The level of nodes that are being accounted for during this iteration
92+
* @param parentExpanded Whether the parent of the first node in param nodes is expanded
93+
*/
94+
private _getTreeStructure(nodes: [number, string, boolean][], level: number,
95+
parentExpanded: boolean): TextTree {
96+
const result: TextTree = {};
97+
for (let i = 0; i < nodes.length; i++) {
98+
const [nodeLevel, text, expanded] = nodes[i];
99+
const nextNodeLevel = nodes[i + 1]?.[0] ?? -1;
100+
101+
// Return the accumulated value for the current level once we reach a shallower level node
102+
if (nodeLevel < level) {
103+
return result;
104+
}
105+
// Skip deeper level nodes during this iteration, they will be picked up in a later iteration
106+
if (nodeLevel > level) {
107+
continue;
108+
}
109+
// Only add to representation if it is visible (parent is expanded)
110+
if (parentExpanded) {
111+
// Collect the data under this node according to the following rules:
112+
// 1. If the next node in the list is a sibling of the current node add it to the child list
113+
// 2. If the next node is a child of the current node, get the sub-tree structure for the
114+
// child and add it under this node
115+
// 3. If the next node has a shallower level, we've reached the end of the child nodes for
116+
// the current parent.
117+
if (nextNodeLevel === level) {
118+
this._addChildToNode(result, {text});
119+
} else if (nextNodeLevel > level) {
120+
let children = this._getTreeStructure(nodes.slice(i + 1),
121+
nextNodeLevel,
122+
expanded)?.children;
123+
let child = children ? {text, children} : {text};
124+
this._addChildToNode(result, child);
125+
} else {
126+
this._addChildToNode(result, {text});
127+
return result;
128+
}
129+
}
130+
}
131+
return result;
132+
}
133+
134+
private _addChildToNode(result: TextTree, child: TextTree) {
135+
result.children ? result.children.push(child) : result.children = [child];
136+
}
31137
}

tools/public_api_guard/material/tree/testing.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export declare class MatTreeHarness extends ComponentHarness {
22
getNodes(filter?: TreeNodeHarnessFilters): Promise<MatTreeNodeHarness[]>;
3+
getTreeStructure(): Promise<TextTree>;
34
static hostSelector: string;
45
static with(options?: TreeHarnessFilters): HarnessPredicate<MatTreeHarness>;
56
}
@@ -17,6 +18,11 @@ export declare class MatTreeNodeHarness extends ContentContainerComponentHarness
1718
static with(options?: TreeNodeHarnessFilters): HarnessPredicate<MatTreeNodeHarness>;
1819
}
1920

21+
export declare type TextTree = {
22+
text?: string;
23+
children?: TextTree[];
24+
};
25+
2026
export interface TreeHarnessFilters extends BaseHarnessFilters {
2127
}
2228

0 commit comments

Comments
 (0)