Skip to content

Commit f0aaf22

Browse files
mmalerbaqiyiggrkirovtrshaferCaitlin Mott
committed
feat(cdk-experimental/testing): Bring in component harness
Co-Authored-By: Yi Qi <[email protected]> Co-Authored-By: Rado Kirov <[email protected]> Co-Authored-By: Thomas Shafer <[email protected]> Co-Authored-By: Caitlin Mott <[email protected]> Co-Authored-By: Craig Nishina <[email protected]>
1 parent 461d539 commit f0aaf22

File tree

12 files changed

+1106
-0
lines changed

12 files changed

+1106
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Test Element interface
3+
* This is a wrapper of native element
4+
*/
5+
export interface TestElement {
6+
blur(): Promise<void>;
7+
clear(): Promise<void>;
8+
click(): Promise<void>;
9+
focus(): Promise<void>;
10+
getCssValue(property: string): Promise<string>;
11+
hover(): Promise<void>;
12+
sendKeys(keys: string): Promise<void>;
13+
text(): Promise<string>;
14+
getAttribute(name: string): Promise<string|null>;
15+
}
16+
17+
/**
18+
* Extra searching options used by searching functions
19+
*
20+
* @param allowNull Optional, whether the found element can be null. If
21+
* allowNull is set, the searching function will always try to fetch the element
22+
* at once. When the element cannot be found, the searching function should
23+
* return null if allowNull is set to true, throw an error if allowNull is set
24+
* to false. If allowNull is not set, the framework will choose the behaviors
25+
* that make more sense for each test type (e.g. for unit test, the framework
26+
* will make sure the element is not null; otherwise throw an error); however,
27+
* the internal behavior is not guaranteed and user should not rely on it. Note
28+
* that in most cases, you don't need to care about whether an element is
29+
* present when loading the element and don't need to set this parameter unless
30+
* you do want to check whether the element is present when calling the
31+
* searching function. e.g. you want to make sure some element is not there when
32+
* loading the element in order to check whether a "ngif" works well.
33+
*
34+
* @param global Optional. If global is set to true, the selector will match any
35+
* element on the page and is not limited to the root of the harness. If
36+
* global is unset or set to false, the selector will only find elements under
37+
* the current root.
38+
*/
39+
export interface Options {
40+
allowNull?: boolean;
41+
global?: boolean;
42+
}
43+
44+
/**
45+
* Type narrowing of Options to allow the overloads of ComponentHarness.find to
46+
* return null only if allowNull is set to true.
47+
*/
48+
interface OptionsWithAllowNullSet extends Options {
49+
allowNull: true;
50+
}
51+
52+
/**
53+
* Locator interface
54+
*/
55+
export interface Locator {
56+
/**
57+
* Get the host element of locator.
58+
*/
59+
host(): TestElement;
60+
61+
/**
62+
* Search the first matched test element.
63+
* @param css Selector of the test elements.
64+
* @param options Optional, extra searching options
65+
*/
66+
find(css: string, options?: Options): Promise<TestElement|null>;
67+
68+
/**
69+
* Search all matched test elements under current root by css selector.
70+
* @param css Selector of the test elements.
71+
*/
72+
findAll(css: string): Promise<TestElement[]>;
73+
74+
/**
75+
* Load the first matched Component Harness.
76+
* @param componentHarness Type of user customized harness.
77+
* @param root Css root selector of the new component harness.
78+
* @param options Optional, extra searching options
79+
*/
80+
load<T extends ComponentHarness>(
81+
componentHarness: ComponentHarnessType<T>, root: string,
82+
options?: Options): Promise<T|null>;
83+
84+
/**
85+
* Load all Component Harnesses under current root.
86+
* @param componentHarness Type of user customized harness.
87+
* @param root Css root selector of the new component harnesses.
88+
*/
89+
loadAll<T extends ComponentHarness>(
90+
componentHarness: ComponentHarnessType<T>, root: string): Promise<T[]>;
91+
}
92+
93+
/**
94+
* Base Component Harness
95+
* This base component harness provides the basic ability to locate element and
96+
* sub-component harness. It should be inherited when defining user's own
97+
* harness.
98+
*/
99+
export class ComponentHarness {
100+
constructor(private readonly locator: Locator) {}
101+
102+
/**
103+
* Get the host element of component harness.
104+
*/
105+
host(): TestElement {
106+
return this.locator.host();
107+
}
108+
109+
/**
110+
* Generate a function to find the first matched test element by css
111+
* selector.
112+
* @param css Css selector of the test element.
113+
*/
114+
protected find(css: string): () => Promise<TestElement>;
115+
116+
/**
117+
* Generate a function to find the first matched test element by css
118+
* selector.
119+
* @param css Css selector of the test element.
120+
* @param options Extra searching options
121+
*/
122+
protected find(css: string, options: OptionsWithAllowNullSet):
123+
() => Promise<TestElement|null>;
124+
125+
/**
126+
* Generate a function to find the first matched test element by css
127+
* selector.
128+
* @param css Css selector of the test element.
129+
* @param options Extra searching options
130+
*/
131+
protected find(css: string, options: Options): () => Promise<TestElement>;
132+
133+
/**
134+
* Generate a function to find the first matched Component Harness.
135+
* @param componentHarness Type of user customized harness.
136+
* @param root Css root selector of the new component harness.
137+
*/
138+
protected find<T extends ComponentHarness>(
139+
componentHarness: ComponentHarnessType<T>,
140+
root: string): () => Promise<T>;
141+
142+
/**
143+
* Generate a function to find the first matched Component Harness.
144+
* @param componentHarness Type of user customized harness.
145+
* @param root Css root selector of the new component harness.
146+
* @param options Extra searching options
147+
*/
148+
protected find<T extends ComponentHarness>(
149+
componentHarness: ComponentHarnessType<T>, root: string,
150+
options: OptionsWithAllowNullSet): () => Promise<T|null>;
151+
152+
/**
153+
* Generate a function to find the first matched Component Harness.
154+
* @param componentHarness Type of user customized harness.
155+
* @param root Css root selector of the new component harness.
156+
* @param options Extra searching options
157+
*/
158+
protected find<T extends ComponentHarness>(
159+
componentHarness: ComponentHarnessType<T>, root: string,
160+
options: Options): () => Promise<T>;
161+
162+
protected find<T extends ComponentHarness>(
163+
cssOrComponentHarness: string|ComponentHarnessType<T>,
164+
cssOrOptions?: string|Options,
165+
options?: Options): () => Promise<TestElement|T|null> {
166+
if (typeof cssOrComponentHarness === 'string') {
167+
const css = cssOrComponentHarness;
168+
const options = cssOrOptions as Options;
169+
return () => this.locator.find(css, options);
170+
} else {
171+
const componentHarness = cssOrComponentHarness;
172+
const css = cssOrOptions as string;
173+
return () => this.locator.load(componentHarness, css, options);
174+
}
175+
}
176+
177+
/**
178+
* Generate a function to find all matched test elements by css selector.
179+
* @param css Css root selector of elements. It will locate
180+
* elements under the current root.
181+
*/
182+
protected findAll(css: string): () => Promise<TestElement[]>;
183+
184+
/**
185+
* Generate a function to find all Component Harnesses under current
186+
* component harness.
187+
* @param componentHarness Type of user customized harness.
188+
* @param root Css root selector of the new component harnesses. It will
189+
* locate harnesses under the current root.
190+
*/
191+
protected findAll<T extends ComponentHarness>(
192+
componentHarness: ComponentHarnessType<T>,
193+
root: string): () => Promise<T[]>;
194+
195+
protected findAll<T extends ComponentHarness>(
196+
cssOrComponentHarness: string|ComponentHarnessType<T>,
197+
root?: string): () => Promise<TestElement[]|T[]> {
198+
if (typeof cssOrComponentHarness === 'string') {
199+
const css = cssOrComponentHarness;
200+
return () => this.locator.findAll(css);
201+
} else {
202+
const componentHarness = cssOrComponentHarness;
203+
return () => this.locator.loadAll(componentHarness, root as string);
204+
}
205+
}
206+
}
207+
208+
/**
209+
* Type of ComponentHarness.
210+
*/
211+
export interface ComponentHarnessType<T extends ComponentHarness> {
212+
new(locator: Locator): T;
213+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {browser, by, element, ElementFinder} from 'protractor';
2+
import {promise as wdpromise} from 'selenium-webdriver';
3+
4+
import {ComponentHarness, ComponentHarnessType, Locator, Options, TestElement} from './component-harness';
5+
6+
/**
7+
* Component harness factory for protractor.
8+
* The function will not try to fetch the host element of harness at once, which
9+
* is for performance purpose; however, this is the most common way to load
10+
* protractor harness. If you do care whether the host element is present when
11+
* loading harness, using the load function that accepts extra searching
12+
* options.
13+
* @param componentHarness: Type of user defined harness.
14+
* @param rootSelector: Optional. Css selector to specify the root of component.
15+
* Set to 'body' by default
16+
*/
17+
export async function load<T extends ComponentHarness>(
18+
componentHarness: ComponentHarnessType<T>,
19+
rootSelector: string): Promise<T>;
20+
21+
/**
22+
* Component harness factory for protractor.
23+
* @param componentHarness: Type of user defined harness.
24+
* @param rootSelector: Optional. Css selector to specify the root of component.
25+
* Set to 'body' by default.
26+
* @param options Optional. Extra searching options
27+
*/
28+
export async function load<T extends ComponentHarness>(
29+
componentHarness: ComponentHarnessType<T>, rootSelector?: string,
30+
options?: Options): Promise<T|null>;
31+
32+
export async function load<T extends ComponentHarness>(
33+
componentHarness: ComponentHarnessType<T>, rootSelector = 'body',
34+
options?: Options): Promise<T|null> {
35+
const root = await getElement(rootSelector, undefined, options);
36+
if (root === null) {
37+
return null;
38+
}
39+
const locator = new ProtractorLocator(root);
40+
return new componentHarness(locator);
41+
}
42+
43+
/**
44+
* Gets the corresponding ElementFinder for the root of a TestElement.
45+
*/
46+
export function getElementFinder(testElement: TestElement): ElementFinder {
47+
if (testElement instanceof ProtractorElement) {
48+
return testElement.element;
49+
}
50+
51+
throw new Error('Invalid element provided');
52+
}
53+
54+
class ProtractorLocator implements Locator {
55+
private root: ProtractorElement;
56+
constructor(private rootFinder: ElementFinder) {
57+
this.root = new ProtractorElement(this.rootFinder);
58+
}
59+
60+
host(): TestElement {
61+
return this.root;
62+
}
63+
64+
async find(css: string, options?: Options): Promise<TestElement|null> {
65+
const finder = await getElement(css, this.rootFinder, options);
66+
if (finder === null) return null;
67+
return new ProtractorElement(finder);
68+
}
69+
70+
async findAll(css: string): Promise<TestElement[]> {
71+
const elementFinders = this.rootFinder.all(by.css(css));
72+
const res: TestElement[] = [];
73+
await elementFinders.each(el => {
74+
if (el) {
75+
res.push(new ProtractorElement(el));
76+
}
77+
});
78+
return res;
79+
}
80+
81+
async load<T extends ComponentHarness>(
82+
componentHarness: ComponentHarnessType<T>, css: string,
83+
options?: Options): Promise<T|null> {
84+
const root = await getElement(css, this.rootFinder, options);
85+
if (root === null) return null;
86+
const locator = new ProtractorLocator(root);
87+
return new componentHarness(locator);
88+
}
89+
90+
async loadAll<T extends ComponentHarness>(
91+
componentHarness: ComponentHarnessType<T>,
92+
rootSelector: string,
93+
): Promise<T[]> {
94+
const roots = this.rootFinder.all(by.css(rootSelector));
95+
const res: T[] = [];
96+
await roots.each(el => {
97+
if (el) {
98+
const locator = new ProtractorLocator(el);
99+
res.push(new componentHarness(locator));
100+
}
101+
});
102+
return res;
103+
}
104+
}
105+
106+
class ProtractorElement implements TestElement {
107+
constructor(readonly element: ElementFinder) {}
108+
109+
blur(): Promise<void> {
110+
return toPromise<void>(this.element['blur']());
111+
}
112+
113+
clear(): Promise<void> {
114+
return toPromise<void>(this.element.clear());
115+
}
116+
117+
click(): Promise<void> {
118+
return toPromise<void>(this.element.click());
119+
}
120+
121+
focus(): Promise<void> {
122+
return toPromise<void>(this.element['focus']());
123+
}
124+
125+
getCssValue(property: string): Promise<string> {
126+
return toPromise<string>(this.element.getCssValue(property));
127+
}
128+
129+
async hover(): Promise<void> {
130+
return toPromise<void>(browser.actions()
131+
.mouseMove(await this.element.getWebElement())
132+
.perform());
133+
}
134+
135+
sendKeys(keys: string): Promise<void> {
136+
return toPromise<void>(this.element.sendKeys(keys));
137+
}
138+
139+
text(): Promise<string> {
140+
return toPromise(this.element.getText());
141+
}
142+
143+
getAttribute(name: string): Promise<string|null> {
144+
return toPromise(this.element.getAttribute(name));
145+
}
146+
}
147+
148+
function toPromise<T>(p: wdpromise.Promise<T>): Promise<T> {
149+
return new Promise<T>((resolve, reject) => {
150+
p.then(resolve, reject);
151+
});
152+
}
153+
154+
/**
155+
* Get an element finder based on the css selector and root element.
156+
* Note that it will check whether the element is present only when
157+
* Options.allowNull is set. This is for performance purpose.
158+
* @param css the css selector
159+
* @param root Optional Search element under the root element. If not set,
160+
* search element globally. If options.global is set, root is ignored.
161+
* @param options Optional, extra searching options
162+
*/
163+
async function getElement(css: string, root?: ElementFinder, options?: Options):
164+
Promise<ElementFinder|null> {
165+
const useGlobalRoot = options && !!options.global;
166+
const elem = root === undefined || useGlobalRoot ? element(by.css(css)) :
167+
root.element(by.css(css));
168+
const allowNull = options !== undefined && options.allowNull !== undefined ?
169+
options.allowNull :
170+
undefined;
171+
if (allowNull !== undefined) {
172+
// Only check isPresent when allowNull is set
173+
if (!(await elem.isPresent())) {
174+
if (allowNull) {
175+
return null;
176+
}
177+
throw new Error('Cannot find element based on the css selector: ' + css);
178+
}
179+
return elem;
180+
}
181+
return elem;
182+
}

0 commit comments

Comments
 (0)