Skip to content

feat(material/tree): Add test harness for MatTree #20018

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

Closed
wants to merge 4 commits into from
Closed
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 @@ -62,6 +62,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';
123 changes: 123 additions & 0 deletions src/material/tree/testing/node-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @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';

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW the intent is that this directive is just one way someone could add a toggle for a node. We may want to make the harness more flexible in terms of how expand/collapse works.


/**
* 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.. */
async getLevel(): Promise<number> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be clear about whether this is zero-based or one-based (see comments on #17818)

return coerceNumberProperty(await (await this.host()).getAttribute('aria-level'));
}

/** Whether the node is a leaf node. */
async isLeaf(): Promise<boolean> {
const role = await (await this.host()).getAttribute('role');
if (role === 'group') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI the way we use role="group" now is probably wrong and they should all be treeitem (related #17818).

return false;
} else if (role === 'treeitem') {
return true
} else {
throw new Error('Invalid node role');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new Error('Invalid node role');
throw Error('Invalid node role');

You don't actually need new with Error (this is different from code written inside Google where new is required just for consistency)

}
}

/** Gets the tree node's text. */
async getText(): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the nested tree node this will get the text of the node and everything under it, you might want to have separate implementations of this method for the different subclasses

return (await this.host()).text();
}

/** 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();
}
}
}

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

/**
* Gets a `HarnessPredicate` that can be used to search for
* a nested 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<MatNestedTreeNodeHarness> {
return getNodePredicate(MatNestedTreeNodeHarness, options);
}
}


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)
.addOption(
'leaf', options.leaf,
async (harness, leaf) => (await harness.isLeaf()) === leaf);
}
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';
Loading