Skip to content
This repository was archived by the owner on Jan 3, 2019. It is now read-only.

Commit d1ca24a

Browse files
authored
feat(all): general api improvements (#201)
- Add `select` method to the `Component` class - Improve error messages - Hide implementation details of the `TestStep` class - The `WebComponent` class is now abstract BREAKING CHANGES: Breaking API changes to the `TestStep` and `WebComponent` classes
1 parent d8d188f commit d1ca24a

29 files changed

+1421
-174
lines changed

@pageobject/base/src/Component.test.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ class DIV extends Component<HTMLElement> {
1515
public static readonly selector: string = 'div';
1616

1717
public get divs(): DIV {
18-
return new DIV(this.adapter, this);
18+
return this.select(DIV);
1919
}
2020

2121
public getID(): Effect<string> {
2222
return async () => (await this.findUniqueNode()).id;
2323
}
2424
}
2525

26+
class Unselectable extends Component<HTMLElement> {}
27+
2628
document.body.innerHTML = `
2729
<div id="a1">
2830
<div id="b1">
@@ -35,7 +37,7 @@ document.body.innerHTML = `
3537
`;
3638

3739
const adapter = new TestAdapter();
38-
const component = new Component(adapter);
40+
const unselectable = new Unselectable(adapter);
3941
const divs = new DIV(adapter);
4042

4143
const a1List = [
@@ -96,19 +98,23 @@ const filterNotUnique = divs.where(div => div.divs.getID(), matches(/./));
9698

9799
describe('Component', () => {
98100
describe('at()', () => {
99-
it('should throw a position-one-based error', () => {
100-
expect(() => divs.at(0)).toThrow('Position must be one-based');
101+
it('should throw an illegal-argument error', () => {
102+
expect(() => divs.at(0)).toThrow(
103+
'The specified position (0) must be one-based'
104+
);
101105
});
102106

103-
it('should throw a position-already-set error', () => {
104-
expect(() => divs.at(1).at(2)).toThrow('Position is already set');
107+
it('should throw an illegal-state error', () => {
108+
expect(() => divs.at(1).at(2)).toThrow(
109+
'The existing position (1) of this <DIV> component cannot be overwritten with 2'
110+
);
105111
});
106112
});
107113

108114
describe('findUniqueNode()', () => {
109-
it('should throw an undefined-selector error', async () => {
110-
await expect(component.findUniqueNode()).rejects.toThrow(
111-
'Undefined selector'
115+
it('should throw an illegal-argument error', async () => {
116+
await expect(unselectable.findUniqueNode()).rejects.toThrow(
117+
'The specified <Unselectable> component has no selector'
112118
);
113119
});
114120

@@ -130,8 +136,8 @@ describe('Component', () => {
130136
}
131137
});
132138

133-
it('should throw a node-not-found error', async () => {
134-
const message = 'Node not found: DIV';
139+
it('should throw a component-not-found error', async () => {
140+
const message = 'The searched <DIV> component cannot be found';
135141

136142
for (const notFound of notFoundList) {
137143
await expect(notFound.findUniqueNode()).rejects.toThrow(message);
@@ -141,8 +147,9 @@ describe('Component', () => {
141147
await expect(filterNotFound.findUniqueNode()).rejects.toThrow(message);
142148
});
143149

144-
it('should throw a node-not-unique error', async () => {
145-
const message = 'Node not unique: DIV';
150+
it('should throw a component-not-unique error', async () => {
151+
const message =
152+
'The searched <DIV> component cannot be uniquely determined';
146153

147154
for (const notUnique of notUniqueList) {
148155
await expect(notUnique.findUniqueNode()).rejects.toThrow(message);
@@ -154,9 +161,9 @@ describe('Component', () => {
154161
});
155162

156163
describe('getNodeCount() => Effect()', () => {
157-
it('should throw an undefined-selector error', async () => {
158-
await expect(component.getNodeCount()()).rejects.toThrow(
159-
'Undefined selector'
164+
it('should throw an illegal-argument error', async () => {
165+
await expect(unselectable.getNodeCount()()).rejects.toThrow(
166+
'The specified <Unselectable> component has no selector'
160167
);
161168
});
162169

@@ -190,15 +197,16 @@ describe('Component', () => {
190197
}
191198
});
192199

193-
it('should throw a node-not-found error', async () => {
194-
const message = 'Node not found: DIV';
200+
it('should throw a component-not-found error', async () => {
201+
const message = 'The searched <DIV> component cannot be found';
195202

196203
await expect(ancestorNotFound.getNodeCount()()).rejects.toThrow(message);
197204
await expect(filterNotFound.getNodeCount()()).rejects.toThrow(message);
198205
});
199206

200-
it('should throw a node-not-unique error', async () => {
201-
const message = 'Node not unique: DIV';
207+
it('should throw a component-not-unique error', async () => {
208+
const message =
209+
'The searched <DIV> component cannot be uniquely determined';
202210

203211
await expect(ancestorNotUnique.getNodeCount()()).rejects.toThrow(message);
204212
await expect(filterNotUnique.getNodeCount()()).rejects.toThrow(message);

@pageobject/base/src/Component.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ export interface Adapter<TNode> {
44
findNodes(selector: string, ancestor?: TNode): Promise<TNode[]>;
55
}
66

7+
export interface ComponentClass<TNode, TComponent extends Component<TNode>> {
8+
readonly selector: string;
9+
10+
new (adapter: Adapter<TNode>, ancestor?: Component<TNode>): TComponent;
11+
}
12+
713
export type Effect<TResult> = () => Promise<TResult>;
814

915
export type Getter<TNode, TComponent extends Component<TNode>, TResult> = (
1016
component: TComponent
1117
) => Effect<TResult>;
1218

13-
export class Component<TNode> {
19+
export abstract class Component<TNode> {
1420
public static readonly selector: string | undefined;
1521

1622
public readonly adapter: Adapter<TNode>;
@@ -25,13 +31,23 @@ export class Component<TNode> {
2531
this.ancestor = ancestor;
2632
}
2733

34+
public select<TComponent extends Component<TNode>>(
35+
Descendant: ComponentClass<TNode, TComponent>
36+
): TComponent {
37+
return new Descendant(this.adapter, this);
38+
}
39+
2840
public at(position: number): this {
2941
if (position < 1) {
30-
throw new Error('Position must be one-based');
42+
throw new Error(`The specified position (${position}) must be one-based`);
3143
}
3244

3345
if (this._position) {
34-
throw new Error('Position is already set');
46+
throw new Error(
47+
`The existing position (${
48+
this._position
49+
}) of this ${this.toString()} component cannot be overwritten with ${position}`
50+
);
3551
}
3652

3753
const reconstruction = this._reconstruct();
@@ -65,7 +81,9 @@ export class Component<TNode> {
6581
const {selector} = this.constructor as typeof Component;
6682

6783
if (!selector) {
68-
throw new Error('Undefined selector');
84+
throw new Error(
85+
`The specified ${this.toString()} component has no selector`
86+
);
6987
}
7088

7189
let nodes = await this.adapter.findNodes(
@@ -104,11 +122,15 @@ export class Component<TNode> {
104122
const nodes = await this.findNodes();
105123

106124
if (nodes.length === 0) {
107-
throw new Error(`Node not found: ${this.constructor.name}`);
125+
throw new Error(
126+
`The searched ${this.toString()} component cannot be found`
127+
);
108128
}
109129

110130
if (nodes.length > 1) {
111-
throw new Error(`Node not unique: ${this.constructor.name}`);
131+
throw new Error(
132+
`The searched ${this.toString()} component cannot be uniquely determined`
133+
);
112134
}
113135

114136
return nodes[0];
@@ -118,10 +140,14 @@ export class Component<TNode> {
118140
return async () => (await this.findNodes()).length;
119141
}
120142

143+
public toString(): string {
144+
return `<${this.constructor.name}>`;
145+
}
146+
121147
private _reconstruct(): this {
122-
return new (this.constructor as typeof Component)(
148+
return new (this.constructor as ComponentClass<TNode, this>)(
123149
this.adapter,
124150
this.ancestor
125-
) as this;
151+
);
126152
}
127153
}

@pageobject/base/src/TestStep.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export class TestStep {
66
public static assert<TValue>(
77
effect: Effect<TValue>,
88
predicate: Predicate<TValue>,
9-
timeoutInSeconds?: number
9+
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
1010
): TestStep {
1111
return new TestStep(
1212
async () => {
@@ -24,7 +24,7 @@ export class TestStep {
2424
predicate: Predicate<TValue>,
2525
thenTestSteps: TestStep[],
2626
elseTestSteps: TestStep[] = [],
27-
timeoutInSeconds?: number
27+
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
2828
): TestStep {
2929
return new TestStep(
3030
async () =>
@@ -36,7 +36,7 @@ export class TestStep {
3636

3737
public static perform(
3838
effect: Effect<void>,
39-
timeoutInSeconds?: number
39+
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
4040
): TestStep {
4141
return new TestStep(
4242
async () => {
@@ -55,22 +55,26 @@ export class TestStep {
5555
}
5656
}
5757

58-
public readonly effect: Effect<TestStep[]>;
59-
public readonly retryOnError: boolean;
60-
public readonly timeoutInSeconds: number;
58+
private readonly _effect: Effect<TestStep[]>;
59+
private readonly _retryOnError: boolean;
60+
private readonly _timeoutInSeconds: number;
6161

62-
public constructor(
62+
private constructor(
6363
effect: Effect<TestStep[]>,
6464
retryOnError: boolean,
65-
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
65+
timeoutInSeconds: number
6666
) {
67-
this.effect = effect;
68-
this.retryOnError = retryOnError;
69-
this.timeoutInSeconds = timeoutInSeconds;
67+
this._effect = effect;
68+
this._retryOnError = retryOnError;
69+
this._timeoutInSeconds = timeoutInSeconds;
7070
}
7171

7272
public async run(): Promise<TestStep[]> {
73-
const {effect, retryOnError, timeoutInSeconds} = this;
73+
const {
74+
_effect: effect,
75+
_retryOnError: retryOnError,
76+
_timeoutInSeconds: timeoutInSeconds
77+
} = this;
7478

7579
let message = `Timeout after ${timeoutInSeconds} second${
7680
timeoutInSeconds === 1 ? '' : 's'

@pageobject/web/src/WebBrowser.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ describe('WebBrowser', () => {
5151
});
5252

5353
describe('press()', () => {
54-
it('should throw a single-character error', () => {
55-
const errorMessage =
56-
"Key must be a single character or one of: 'Enter', 'Escape', 'Tab'";
54+
it('should throw an illegal-argument error', () => {
55+
expect(() => browser.press('')).toThrow(
56+
"The specified key ('') must be a single character or one of: 'Enter', 'Escape', 'Tab'"
57+
);
5758

58-
expect(() => browser.press('')).toThrow(errorMessage);
59-
expect(() => browser.press('aa')).toThrow(errorMessage);
59+
expect(() => browser.press('aa')).toThrow(
60+
"The specified key ('aa') must be a single character or one of: 'Enter', 'Escape', 'Tab'"
61+
);
6062
});
6163

6264
describe('=> Effect()', () => {

@pageobject/web/src/WebBrowser.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Adapter, Effect} from '@pageobject/base';
2+
import {inspect} from 'util';
23
import {Argument, WebNode} from '.';
34

45
export type Key = 'Enter' | 'Escape' | 'Tab' | string;
@@ -14,6 +15,11 @@ export interface WebAdapter extends Adapter<WebNode> {
1415
quit(): Promise<void>;
1516
}
1617

18+
// tslint:disable-next-line no-any
19+
function serialize(value: any): string {
20+
return inspect(value, false, null);
21+
}
22+
1723
export class WebBrowser {
1824
public readonly adapter: WebAdapter;
1925

@@ -43,7 +49,9 @@ export class WebBrowser {
4349
}
4450
default: {
4551
throw new Error(
46-
"Key must be a single character or one of: 'Enter', 'Escape', 'Tab'"
52+
`The specified key (${serialize(
53+
key
54+
)}) must be a single character or one of: 'Enter', 'Escape', 'Tab'`
4755
);
4856
}
4957
}

@pageobject/web/src/WebComponent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface WebNode {
1212
): Promise<TResult>;
1313
}
1414

15-
export class WebComponent extends Component<WebNode> {
15+
export abstract class WebComponent extends Component<WebNode> {
1616
public click(): Effect<void> {
1717
return async () => (await this.findUniqueNode()).click();
1818
}

docs/api/base/assets/js/search.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/base/classes/binarypredicate.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,9 @@
984984
<li class=" tsd-kind-interface tsd-has-type-parameter">
985985
<a href="../interfaces/adapter.html" class="tsd-kind-icon">Adapter</a>
986986
</li>
987+
<li class=" tsd-kind-interface tsd-has-type-parameter">
988+
<a href="../interfaces/componentclass.html" class="tsd-kind-icon">Component<wbr>Class</a>
989+
</li>
987990
<li class=" tsd-kind-type-alias tsd-has-type-parameter">
988991
<a href="../globals.html#effect" class="tsd-kind-icon">Effect</a>
989992
</li>

0 commit comments

Comments
 (0)