Skip to content

feat(material/tree): add getTreeStructure for tree harness #20568

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 3 commits into from
Oct 14, 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
117 changes: 109 additions & 8 deletions src/material/tree/testing/shared.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,115 @@ export function runHarnessTests(
// no-op if already collapsed
expect(await firstGroup.isExpanded()).toBe(false);
});

it ('should correctly get tree structure', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const flatTree = trees[0];

expect(await flatTree.getTreeStructure()).toEqual({
children: [
{text: 'Flat Group 1'},
{text: 'Flat Group 2'}
]
});

const firstGroup = (await flatTree.getNodes({text: /Flat Group 1/}))[0];
await firstGroup.expand();

expect(await flatTree.getTreeStructure()).toEqual({
children: [
{
text: 'Flat Group 1',
children: [
{text: 'Flat Leaf 1.1'},
{text: 'Flat Leaf 1.2'},
{text: 'Flat Leaf 1.3'}
]
},
{text: 'Flat Group 2'}
]
});

const secondGroup = (await flatTree.getNodes({text: /Flat Group 2/}))[0];
await secondGroup.expand();

expect(await flatTree.getTreeStructure()).toEqual({
children: [
{
text: 'Flat Group 1',
children: [
{text: 'Flat Leaf 1.1'},
{text: 'Flat Leaf 1.2'},
{text: 'Flat Leaf 1.3'}
]
},
{
text: 'Flat Group 2',
children: [
{text: 'Flat Group 2.1'},
]
}
]
});
});

it('should correctly get tree structure', async () => {
const trees = await loader.getAllHarnesses(treeHarness);
const nestedTree = trees[1];
expect(await nestedTree.getTreeStructure()).toEqual({
children: [
{text: 'Nested Group 1'},
{text: 'Nested Group 2'}
]
});

const firstGroup = (await nestedTree.getNodes({text: /Nested Group 1/}))[0];
await firstGroup.expand();
expect(await nestedTree.getTreeStructure()).toEqual(
{
children: [
{
text: 'Nested Group 1',
children: [
{text: 'Nested Leaf 1.1'},
{text: 'Nested Leaf 1.2'},
{text: 'Nested Leaf 1.3'}
]
},
{text: 'Nested Group 2'}
]
});

const secondGroup = (await nestedTree.getNodes({text: /Nested Group 2/}))[0];
await secondGroup.expand();
expect(await nestedTree.getTreeStructure()).toEqual(
{
children: [
{
text: 'Nested Group 1',
children: [
{text: 'Nested Leaf 1.1'},
{text: 'Nested Leaf 1.2'},
{text: 'Nested Leaf 1.3'}
]
},
{
text: 'Nested Group 2',
children: [
{text: 'Nested Group 2.1'},
]
}
]
});
});
}

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

const FLAT_TREE_DATA: FoodNode[] = [
const FLAT_TREE_DATA: Node[] = [
{
name: 'Flat Group 1',
children: [
Expand All @@ -126,7 +227,7 @@ const FLAT_TREE_DATA: FoodNode[] = [
},
];

const NESTED_TREE_DATA: FoodNode[] = [
const NESTED_TREE_DATA: Node[] = [
{
name: 'Nested Group 1',
children: [
Expand Down Expand Up @@ -188,7 +289,7 @@ interface ExampleFlatNode {
`
})
class TreeHarnessTest {
private _transformer = (node: FoodNode, level: number) => {
private _transformer = (node: Node, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
name: node.name,
Expand All @@ -201,8 +302,8 @@ class TreeHarnessTest {
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>();
nestedTreeControl = new NestedTreeControl<Node>(node => node.children);
nestedTreeDataSource = new MatTreeNestedDataSource<Node>();

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

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

nestedTreeHasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
nestedTreeHasChild = (_: number, node: Node) => !!node.children && node.children.length > 0;
}
108 changes: 107 additions & 1 deletion src/material/tree/testing/tree-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/

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

export type TextTree = {
text?: string;
children?: TextTree[];
};

/** Harness for interacting with a standard mat-tree in tests. */
export class MatTreeHarness extends ComponentHarness {
/** The selector for the host element of a `MatTableHarness` instance. */
Expand All @@ -28,4 +33,105 @@ export class MatTreeHarness extends ComponentHarness {
async getNodes(filter: TreeNodeHarnessFilters = {}): Promise<MatTreeNodeHarness[]> {
return this.locatorForAll(MatTreeNodeHarness.with(filter))();
}

/**
* Gets an object representation for the visible tree structure
* If a node is under an unexpanded node it will not be included.
* Eg.
* Tree (all nodes expanded):
* `
* <mat-tree>
* <mat-tree-node>Node 1<mat-tree-node>
* <mat-nested-tree-node>
* Node 2
* <mat-nested-tree-node>
* Node 2.1
* <mat-tree-node>
* Node 2.1.1
* <mat-tree-node>
* <mat-nested-tree-node>
* <mat-tree-node>
* Node 2.2
* <mat-tree-node>
* <mat-nested-tree-node>
* </mat-tree>`
*
* Tree structure:
* {
* children: [
* {
* text: 'Node 1',
* children: [
* {
* text: 'Node 2',
* children: [
* {
* text: 'Node 2.1',
* children: [{text: 'Node 2.1.1'}]
* },
* {text: 'Node 2.2'}
* ]
* }
* ]
* }
* ]
* };
*/
async getTreeStructure(): Promise<TextTree> {
const nodes = await this.getNodes();
const nodeInformation = await parallel(() => nodes.map(node => {
return Promise.all([node.getLevel(), node.getText(), node.isExpanded()]);
}));
return this._getTreeStructure(nodeInformation, 1, true);
}

/**
* Recursively collect the structured text of the tree nodes.
* @param nodes A list of tree nodes
* @param level The level of nodes that are being accounted for during this iteration
* @param parentExpanded Whether the parent of the first node in param nodes is expanded
*/
private _getTreeStructure(nodes: [number, string, boolean][], level: number,
parentExpanded: boolean): TextTree {
const result: TextTree = {};
for (let i = 0; i < nodes.length; i++) {
const [nodeLevel, text, expanded] = nodes[i];
const nextNodeLevel = nodes[i + 1]?.[0] ?? -1;

// Return the accumulated value for the current level once we reach a shallower level node
if (nodeLevel < level) {
return result;
}
// Skip deeper level nodes during this iteration, they will be picked up in a later iteration
if (nodeLevel > level) {
continue;
}
// Only add to representation if it is visible (parent is expanded)
if (parentExpanded) {
// Collect the data under this node according to the following rules:
// 1. If the next node in the list is a sibling of the current node add it to the child list
// 2. If the next node is a child of the current node, get the sub-tree structure for the
// child and add it under this node
// 3. If the next node has a shallower level, we've reached the end of the child nodes for
// the current parent.
if (nextNodeLevel === level) {
this._addChildToNode(result, {text});
} else if (nextNodeLevel > level) {
let children = this._getTreeStructure(nodes.slice(i + 1),
nextNodeLevel,
expanded)?.children;
let child = children ? {text, children} : {text};
this._addChildToNode(result, child);
} else {
this._addChildToNode(result, {text});
return result;
}
}
}
return result;
}

private _addChildToNode(result: TextTree, child: TextTree) {
result.children ? result.children.push(child) : result.children = [child];
}
}
6 changes: 6 additions & 0 deletions tools/public_api_guard/material/tree/testing.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export declare class MatTreeHarness extends ComponentHarness {
getNodes(filter?: TreeNodeHarnessFilters): Promise<MatTreeNodeHarness[]>;
getTreeStructure(): Promise<TextTree>;
static hostSelector: string;
static with(options?: TreeHarnessFilters): HarnessPredicate<MatTreeHarness>;
}
Expand All @@ -17,6 +18,11 @@ export declare class MatTreeNodeHarness extends ComponentHarness {
static with(options?: TreeNodeHarnessFilters): HarnessPredicate<MatTreeNodeHarness>;
}

export declare type TextTree = {
text?: string;
children?: TextTree[];
};

export interface TreeHarnessFilters extends BaseHarnessFilters {
}

Expand Down