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

Commit

Permalink
feat(all): general api improvements (#201)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
clebert committed Apr 18, 2018
1 parent d8d188f commit d1ca24a
Show file tree
Hide file tree
Showing 29 changed files with 1,421 additions and 174 deletions.
48 changes: 28 additions & 20 deletions @pageobject/base/src/Component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ class DIV extends Component<HTMLElement> {
public static readonly selector: string = 'div';

public get divs(): DIV {
return new DIV(this.adapter, this);
return this.select(DIV);
}

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

class Unselectable extends Component<HTMLElement> {}

document.body.innerHTML = `
<div id="a1">
<div id="b1">
Expand All @@ -35,7 +37,7 @@ document.body.innerHTML = `
`;

const adapter = new TestAdapter();
const component = new Component(adapter);
const unselectable = new Unselectable(adapter);
const divs = new DIV(adapter);

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

describe('Component', () => {
describe('at()', () => {
it('should throw a position-one-based error', () => {
expect(() => divs.at(0)).toThrow('Position must be one-based');
it('should throw an illegal-argument error', () => {
expect(() => divs.at(0)).toThrow(
'The specified position (0) must be one-based'
);
});

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

describe('findUniqueNode()', () => {
it('should throw an undefined-selector error', async () => {
await expect(component.findUniqueNode()).rejects.toThrow(
'Undefined selector'
it('should throw an illegal-argument error', async () => {
await expect(unselectable.findUniqueNode()).rejects.toThrow(
'The specified <Unselectable> component has no selector'
);
});

Expand All @@ -130,8 +136,8 @@ describe('Component', () => {
}
});

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

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

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

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

describe('getNodeCount() => Effect()', () => {
it('should throw an undefined-selector error', async () => {
await expect(component.getNodeCount()()).rejects.toThrow(
'Undefined selector'
it('should throw an illegal-argument error', async () => {
await expect(unselectable.getNodeCount()()).rejects.toThrow(
'The specified <Unselectable> component has no selector'
);
});

Expand Down Expand Up @@ -190,15 +197,16 @@ describe('Component', () => {
}
});

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

await expect(ancestorNotFound.getNodeCount()()).rejects.toThrow(message);
await expect(filterNotFound.getNodeCount()()).rejects.toThrow(message);
});

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

await expect(ancestorNotUnique.getNodeCount()()).rejects.toThrow(message);
await expect(filterNotUnique.getNodeCount()()).rejects.toThrow(message);
Expand Down
42 changes: 34 additions & 8 deletions @pageobject/base/src/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ export interface Adapter<TNode> {
findNodes(selector: string, ancestor?: TNode): Promise<TNode[]>;
}

export interface ComponentClass<TNode, TComponent extends Component<TNode>> {
readonly selector: string;

new (adapter: Adapter<TNode>, ancestor?: Component<TNode>): TComponent;
}

export type Effect<TResult> = () => Promise<TResult>;

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

export class Component<TNode> {
export abstract class Component<TNode> {
public static readonly selector: string | undefined;

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

public select<TComponent extends Component<TNode>>(
Descendant: ComponentClass<TNode, TComponent>
): TComponent {
return new Descendant(this.adapter, this);
}

public at(position: number): this {
if (position < 1) {
throw new Error('Position must be one-based');
throw new Error(`The specified position (${position}) must be one-based`);
}

if (this._position) {
throw new Error('Position is already set');
throw new Error(
`The existing position (${
this._position
}) of this ${this.toString()} component cannot be overwritten with ${position}`
);
}

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

if (!selector) {
throw new Error('Undefined selector');
throw new Error(
`The specified ${this.toString()} component has no selector`
);
}

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

if (nodes.length === 0) {
throw new Error(`Node not found: ${this.constructor.name}`);
throw new Error(
`The searched ${this.toString()} component cannot be found`
);
}

if (nodes.length > 1) {
throw new Error(`Node not unique: ${this.constructor.name}`);
throw new Error(
`The searched ${this.toString()} component cannot be uniquely determined`
);
}

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

public toString(): string {
return `<${this.constructor.name}>`;
}

private _reconstruct(): this {
return new (this.constructor as typeof Component)(
return new (this.constructor as ComponentClass<TNode, this>)(
this.adapter,
this.ancestor
) as this;
);
}
}
28 changes: 16 additions & 12 deletions @pageobject/base/src/TestStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class TestStep {
public static assert<TValue>(
effect: Effect<TValue>,
predicate: Predicate<TValue>,
timeoutInSeconds?: number
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
): TestStep {
return new TestStep(
async () => {
Expand All @@ -24,7 +24,7 @@ export class TestStep {
predicate: Predicate<TValue>,
thenTestSteps: TestStep[],
elseTestSteps: TestStep[] = [],
timeoutInSeconds?: number
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
): TestStep {
return new TestStep(
async () =>
Expand All @@ -36,7 +36,7 @@ export class TestStep {

public static perform(
effect: Effect<void>,
timeoutInSeconds?: number
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
): TestStep {
return new TestStep(
async () => {
Expand All @@ -55,22 +55,26 @@ export class TestStep {
}
}

public readonly effect: Effect<TestStep[]>;
public readonly retryOnError: boolean;
public readonly timeoutInSeconds: number;
private readonly _effect: Effect<TestStep[]>;
private readonly _retryOnError: boolean;
private readonly _timeoutInSeconds: number;

public constructor(
private constructor(
effect: Effect<TestStep[]>,
retryOnError: boolean,
timeoutInSeconds: number = TestStep.defaultTimeoutInSeconds
timeoutInSeconds: number
) {
this.effect = effect;
this.retryOnError = retryOnError;
this.timeoutInSeconds = timeoutInSeconds;
this._effect = effect;
this._retryOnError = retryOnError;
this._timeoutInSeconds = timeoutInSeconds;
}

public async run(): Promise<TestStep[]> {
const {effect, retryOnError, timeoutInSeconds} = this;
const {
_effect: effect,
_retryOnError: retryOnError,
_timeoutInSeconds: timeoutInSeconds
} = this;

let message = `Timeout after ${timeoutInSeconds} second${
timeoutInSeconds === 1 ? '' : 's'
Expand Down
12 changes: 7 additions & 5 deletions @pageobject/web/src/WebBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ describe('WebBrowser', () => {
});

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

expect(() => browser.press('')).toThrow(errorMessage);
expect(() => browser.press('aa')).toThrow(errorMessage);
expect(() => browser.press('aa')).toThrow(
"The specified key ('aa') must be a single character or one of: 'Enter', 'Escape', 'Tab'"
);
});

describe('=> Effect()', () => {
Expand Down
10 changes: 9 additions & 1 deletion @pageobject/web/src/WebBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Adapter, Effect} from '@pageobject/base';
import {inspect} from 'util';
import {Argument, WebNode} from '.';

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

// tslint:disable-next-line no-any
function serialize(value: any): string {
return inspect(value, false, null);
}

export class WebBrowser {
public readonly adapter: WebAdapter;

Expand Down Expand Up @@ -43,7 +49,9 @@ export class WebBrowser {
}
default: {
throw new Error(
"Key must be a single character or one of: 'Enter', 'Escape', 'Tab'"
`The specified key (${serialize(
key
)}) must be a single character or one of: 'Enter', 'Escape', 'Tab'`
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion @pageobject/web/src/WebComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface WebNode {
): Promise<TResult>;
}

export class WebComponent extends Component<WebNode> {
export abstract class WebComponent extends Component<WebNode> {
public click(): Effect<void> {
return async () => (await this.findUniqueNode()).click();
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api/base/assets/js/search.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/api/base/classes/binarypredicate.html
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,9 @@
<li class=" tsd-kind-interface tsd-has-type-parameter">
<a href="../interfaces/adapter.html" class="tsd-kind-icon">Adapter</a>
</li>
<li class=" tsd-kind-interface tsd-has-type-parameter">
<a href="../interfaces/componentclass.html" class="tsd-kind-icon">Component<wbr>Class</a>
</li>
<li class=" tsd-kind-type-alias tsd-has-type-parameter">
<a href="../globals.html#effect" class="tsd-kind-icon">Effect</a>
</li>
Expand Down

0 comments on commit d1ca24a

Please sign in to comment.