This repository has been archived by the owner on Jan 3, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(all): a major redesign of the api (#187)
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
Showing
84 changed files
with
12,049 additions
and
27,799 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.