UNPKG

selenium-state-machine

Version:
263 lines (221 loc) 9.06 kB
import { State, StateProvideData } from './State'; import { DependencyMap, DependencyID, ValueDependency, Dependency } from './Dependency'; import { CriticalError } from './Error'; import { BaseContext } from './StateMachine'; import winston = require('winston'); export type ProvideFunction<TContext extends BaseContext, TDependencyMap extends DependencyMap> = (provide: ProvidePublic<TContext, TDependencyMap>, dependencies: TDependencyMap) => ProvideComplete<TContext, TDependencyMap> | Promise<ProvideComplete<TContext, TDependencyMap>>; /** * Provide interface to provide required dependencies or nothing. */ export interface ProvidePublic<TContext extends BaseContext, TDependencyMap extends DependencyMap> { /** * Provide new dependency. Can be chained. * @param dependency dependency to be updated * @param newValue value which will be stored in the dependency */ dependency<T>(dependency: Dependency<T>, newValue: T): DependencyProvider<TContext, TDependencyMap>; /** * Nothing will be provided by this method. */ nothing(): ProvideNothing<TContext, TDependencyMap>; /** * Get context */ readonly context: TContext; /** * Check if timer has elapsed. * @param name name of the timer in question * @returns boolean signaling its state */ hasElapsedTimer(name: string | ProvideFunction<TContext, TDependencyMap>): boolean; /** * Check if timer is set. * @param name name of the timer in question * @returns boolean signalling availability */ hasTimer(name: string | ProvideFunction<TContext, TDependencyMap>): boolean; } /** * Intermediate provider in case of chaining. */ export interface DependencyProvider<TContext extends BaseContext, TDependencyMap extends DependencyMap> { /** * Provide new dependency. Can be chained. * @param dependency dependency to be updated * @param newValue value which will be stored in the dependency */ dependency<T>(dependency: Dependency<T>, newValue: T): DependencyProvider<TContext, TDependencyMap>; /** * Transition to next state */ next(): ProvideComplete<TContext, TDependencyMap>; /** * Transition to previous state */ previous(): ProvideComplete<TContext, TDependencyMap>; /** * Transition to state by name * @param name name of the next state or function it calls */ transition(name: string | ProvideFunction<TContext, TDependencyMap>): ProvideComplete<TContext, TDependencyMap>; } export interface ProvideNothing<TContext extends BaseContext, TDependencyMap extends DependencyMap> { /** * Transition to itself -- repeat the state function */ tryAgain(): ProvideComplete<TContext, TDependencyMap>; /** * Transition to next state */ next(): ProvideComplete<TContext, TDependencyMap>; /** * Transition to previous state */ previous(): ProvideComplete<TContext, TDependencyMap>; /** * Transition to state by name * @param name name of the next state or function it calls */ transition(name: string | ProvideFunction<TContext, TDependencyMap>): ProvideComplete<TContext, TDependencyMap>; } /** * End provide chain */ export interface ProvideComplete<TContext extends BaseContext, TDependencyMap extends DependencyMap> { /** * Create new timer. Useful when it is not desirable perform WebElement click every state transition. * @param name new name of the timer * @param timeout time after timer will be in state 'elapsed' */ createTimer(name: string | ProvideFunction<TContext, TDependencyMap>, timeout: number): ProvideComplete<TContext, TDependencyMap>; /** * Clear set timer with name. * @param name name of the timer */ clearTimer(name: string | ProvideFunction<TContext, TDependencyMap>): ProvideComplete<TContext, TDependencyMap>; /** * Update context values. Make sure you are using immutable types. * @param data which will take part in new context */ updateContext(data: Partial<TContext>): ProvideComplete<TContext, TDependencyMap>; } export interface TimerData { name: string | ProvideFunction<never, never>, timeout: number } export interface ProvideData<TContext extends BaseContext, TDependencyMap extends DependencyMap> extends StateProvideData<TContext> { logger: winston.Logger, provider: State<TContext, TDependencyMap>, } export class Provide<TContext extends BaseContext, TDependencyMap extends DependencyMap> implements ProvidePublic<TContext, TDependencyMap>, DependencyProvider<TContext, TDependencyMap>, ProvideNothing<TContext, TDependencyMap>, ProvideComplete<TContext, TDependencyMap> { private _repeat: boolean; private _next: boolean; private _prev: boolean; private _nextStateName: string; private _updateMap: DependencyMap; private _createTimers: TimerData[]; private _clearTimers: (string | ProvideFunction<never, never>)[]; private _newContext: TContext; public constructor(private readonly config: ProvideData<TContext, TDependencyMap>) { this._repeat = false; this._prev = false; this._next = false; this._nextStateName = ''; this._updateMap = {}; this._clearTimers = []; this._createTimers = []; this._newContext = this.config.context; } public get context() : TContext { return this._newContext; } public get transitionState() : string { if (this._nextStateName === '') { throw new CriticalError('unexpected state'); } return this._nextStateName; } public get newTimers() : typeof this._createTimers { return this._createTimers; } public get staleTimers() : typeof this._clearTimers { return this._clearTimers; } hasElapsedTimer(name: string | ProvideFunction<TContext, TDependencyMap>): boolean { const stringName = typeof name === 'string' ? name : name.name; return this.config.timers[stringName]?.elapsed(this.config.timeout); } hasTimer(name: string | ProvideFunction<TContext, TDependencyMap>): boolean { const stringName = typeof name === 'string' ? name : name.name; return this.config.timers[stringName] !== undefined; } createTimer(name: string | ProvideFunction<TContext, TDependencyMap>, timeout: number): ProvideComplete<TContext, TDependencyMap> { this._createTimers.push({name, timeout}); return this; } clearTimer(name: string | ProvideFunction<TContext, TDependencyMap>): ProvideComplete<TContext, TDependencyMap> { this._clearTimers.push(name); return this; } updateContext(data: Partial<TContext>): ProvideComplete<TContext, TDependencyMap> { this._newContext = { ...this._newContext, ...data }; return this; } public doesTransition(): boolean { return this._nextStateName !== ''; } public doesRepeat() : boolean { return this._repeat; } public doesGoNext() : boolean { return this._next; } public doesGoPrevious() : boolean { return this._prev; } public get updateMap() : DependencyMap { return this._updateMap; } public tryAgain(): ProvideComplete<TContext, TDependencyMap> { this._repeat = true; return this; } public next(): ProvideComplete<TContext, TDependencyMap> { this._next = true; return this; } public previous(): ProvideComplete<TContext, TDependencyMap> { this._prev = true; return this; } public transition(name: string | ProvideFunction<TContext, TDependencyMap>): ProvideComplete<TContext, TDependencyMap> { this._nextStateName = typeof name === 'string' ? name : name.name; return this; } public nothing(): ProvideNothing<TContext, TDependencyMap> { return this; } public dependency<T>(dependency: Dependency<T>, newValue: T): DependencyProvider<TContext, TDependencyMap> { const dep = dependency as ValueDependency<T>; if (this.has(dep.id)) { throw new CriticalError(`Cannot provide dependency with id "${dep.id}" again.`); } const updatedDependency = dep.set(newValue, this.config.provider as unknown as State<never, never>); this._updateMap[updatedDependency.id] = updatedDependency; this.config.logger.info(`new provider set for dependency "${dependency.name}" with name "${this.config.provider.name}"`); return this; } public has(id: DependencyID): boolean { return this._updateMap[id] !== undefined; } public get<T extends never>(id: DependencyID): Dependency<T> { const item = this._updateMap[id]; if (item) { return item as T; } throw new CriticalError(`could not find dependency with name "${id}".`); } }