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

Commit

Permalink
feat(all): a major redesign of the api (#187)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Redesigned, added, or removed most of the classes.

Base:
- Redesigned Component class
- Added Predicate class
- Added TestStep class
- Removed FunctionCall class
- Removed Operation class
- Removed Operator class

Web:
- Redesigned WebBrowser class
- Redesigned WebComponent class
- Improved JSDOMAdapter class
- Improved WebAdapterTest class

Web adapters:
- Improved ProtractorAdapter class
- Improved PuppeteerAdapter class
- Improved SeleniumAdapter class
  • Loading branch information
clebert committed Apr 17, 2018
1 parent 4520c09 commit 14deb46
Show file tree
Hide file tree
Showing 84 changed files with 12,049 additions and 27,799 deletions.
436 changes: 173 additions & 263 deletions @pageobject/base/src/Component.test.ts

Large diffs are not rendered by default.

205 changes: 79 additions & 126 deletions @pageobject/base/src/Component.ts
Original file line number Diff line number Diff line change
@@ -1,174 +1,127 @@
import {Describable, FunctionCall, Operator} from '.';
import {Predicate} from '.';

export interface Adapter<TElement> {
findElements(selector: string, parent?: TElement): Promise<TElement[]>;
export interface Adapter<TNode> {
findNodes(selector: string, ancestor?: TNode): Promise<TNode[]>;
}

export type Accessor<
TElement,
TComponent extends Component<TElement>,
TResult
> = (pageObject: TComponent) => FunctionCall<TResult>;

export interface Filter<
TElement,
TComponent extends Component<TElement>,
TValue
> {
readonly accessor: Accessor<TElement, TComponent, TValue>;
readonly operator: Operator<TValue>;
}

export interface Locator<TElement, TComponent extends Component<TElement>> {
readonly filters?: Filter<TElement, TComponent, any>[]; // tslint:disable-line no-any
readonly parent?: Component<TElement>;
readonly position?: number;
}

export interface ComponentFactory<
TElement,
TComponent extends Component<TElement>
> {
new (
adapter: Adapter<TElement>,
locator?: Locator<TElement, TComponent>
): TComponent;
}

export abstract class Component<TElement> implements Describable {
public abstract readonly selector: string;
export type Effect<TResult> = () => Promise<TResult>;

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

private readonly _adapter: Adapter<TElement>;
private readonly _locator: Locator<TElement, this>;
export class Component<TNode> {
public static readonly selector: string | undefined;

public constructor(
adapter: Adapter<TElement>,
locator: Locator<TElement, any> = {} // tslint:disable-line no-any
) {
this._adapter = adapter;
this._locator = locator;
protected readonly adapter: Adapter<TNode>;
protected readonly ancestor?: Component<TNode>;

this.description = this._describe();
}
private _filter?: (component: Component<TNode>) => Promise<boolean>;
private _position?: number;
private _node?: TNode;

public select<TDescendant extends Component<TElement>>(
Descendant: ComponentFactory<TElement, TDescendant>
): TDescendant {
return new Descendant(this._adapter, {parent: this});
public constructor(adapter: Adapter<TNode>, ancestor?: Component<TNode>) {
this.adapter = adapter;
this.ancestor = ancestor;
}

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

if (this._locator.position) {
if (this._position) {
throw new Error('Position is already set');
}

const Self = this.constructor as ComponentFactory<TElement, this>;
const reconstruction = this.reconstruct();

reconstruction._filter = this._filter;
reconstruction._position = position;

return new Self(this._adapter, {...this._locator, position});
return reconstruction;
}

public where<TValue>(
accessor: Accessor<TElement, this, TValue>,
operator: Operator<TValue>
getter: Getter<TNode, this, TValue>,
predicate: Predicate<TValue>
): this {
const Self = this.constructor as ComponentFactory<TElement, this>;
const reconstruction = this.reconstruct();

return new Self(this._adapter, {
...this._locator,
filters: [...(this._locator.filters || []), {accessor, operator}]
});
}

public getElementCount(): FunctionCall<number> {
return new FunctionCall(
this,
this.getElementCount.name,
arguments,
async () => (await this._findElements()).length
);
}
reconstruction._filter = async component =>
(this._filter ? await this._filter(component) : true) &&
predicate.test(await getter(component as this)());

protected async findElement(): Promise<TElement> {
const elements = await this._findElements();
reconstruction._position = this._position;

if (elements.length === 0) {
throw new Error('Element not found');
}
return reconstruction;
}

if (elements.length > 1) {
throw new Error('Element not unique');
public async findNodes(): Promise<TNode[]> {
if (this._node) {
return [this._node];
}

return elements[0];
}

private _describe(): string {
const {filters, parent, position} = this._locator;
const {selector} = this.constructor as typeof Component;

const selectDescription = parent
? `${parent.description}.select(${this.constructor.name})`
: `${this.constructor.name}`;
if (!selector) {
throw new Error('Undefined selector');
}

const nthDescription = position ? `.nth(${position})` : '';
let nodes = await this.adapter.findNodes(
selector,
this.ancestor && (await this.ancestor.findUniqueNode())
);

const whereDescription = filters
? `.where(${filters
.map(filter =>
filter.operator.describe(filter.accessor(this).description)
)
.join(', ')})`
: '';
const filter = this._filter;

return `${selectDescription}${nthDescription}${whereDescription}`;
}
if (filter) {
const results = await Promise.all(
nodes.map(async node => {
const reconstruction = this.reconstruct();

private async _filterElements(): Promise<TElement[]> {
const {filters, parent} = this._locator;
reconstruction._node = node;

const elements = await this._adapter.findElements(
this.selector,
parent ? await parent.findElement() : undefined
);
return filter(reconstruction);
})
);

if (!filters) {
return elements;
nodes = nodes.filter((node, index) => results[index]);
}

const results = await Promise.all(
elements.map(async element => {
const Self = this.constructor as ComponentFactory<TElement, this>;
const position = this._position;

const instance = new Self({
findElements: async () => [element]
});
if (position) {
const index = position - 1;

return (await Promise.all(
filters.map(async filter =>
filter.operator.test(await filter.accessor(instance).effect())
)
)).every(result => result);
})
);
nodes = index < nodes.length ? [nodes[index]] : [];
}

return elements.filter((element, index) => results[index]);
return nodes;
}

private async _findElements(): Promise<TElement[]> {
const elements = await this._filterElements();
const {position} = this._locator;
public async findUniqueNode(): Promise<TNode> {
const nodes = await this.findNodes();

if (position) {
const index = position - 1;
if (nodes.length === 0) {
throw new Error(`Node not found: ${this.constructor.name}`);
}

return index < elements.length ? [elements[index]] : [];
if (nodes.length > 1) {
throw new Error(`Node not unique: ${this.constructor.name}`);
}

return elements;
return nodes[0];
}

public getNodeCount(): Effect<number> {
return async () => (await this.findNodes()).length;
}

protected reconstruct(): this {
return new (this.constructor as typeof Component)(
this.adapter,
this.ancestor
) as this;
}
}
24 changes: 0 additions & 24 deletions @pageobject/base/src/FunctionCall.test.ts

This file was deleted.

29 changes: 0 additions & 29 deletions @pageobject/base/src/FunctionCall.ts

This file was deleted.

0 comments on commit 14deb46

Please sign in to comment.