Skip to content

Instantly share code, notes, and snippets.

@mgrybyk
Last active February 4, 2022 01:15
Show Gist options
  • Save mgrybyk/a0714cc9d3d2df75ac3e5907d2f6fb46 to your computer and use it in GitHub Desktop.
Save mgrybyk/a0714cc9d3d2df75ac3e5907d2f6fb46 to your computer and use it in GitHub Desktop.
WebdriverIO - new async api
// proxy.ts after compilation
async function p$(selector) {
return pElementFactory.call(this, selector)
}
async function shadow$(selector) {
return pElementFactory.call(this, selector)
}
async function p$$(selector) {
return [pElementFactory.call(this, selector, 0), pElementFactory.call(this, selector, 1)]
}
async function getText() {
return `text of "${this.selector}"`
}
async function url(url) {
return url
}
const chainableMethods = {
element: { p$, p$$, shadow$ },
browser: { p$, p$$ },
}
const regularMethods = {
element: { getText },
browser: { url },
}
function promiseWrapper(parentContext, fn, name, ...argz) {
const cmdResultPromise = parentContext.prevCall
? parentContext.prevCall.then((d) => fn.call(d, ...argz))
: fn.call(parentContext, ...argz)
const newCtx = { ...parentContext, type: chainableMethods.element[name] ? 'element' : parentContext.type }
newCtx.prevCall = cmdResultPromise
const proxy = new Proxy(
{ ctx: newCtx, promise: cmdResultPromise },
{
get: function (target, prop) {
const pValue = target.promise[prop]
if (pValue !== undefined) {
return typeof pValue === 'function' ? pValue.bind(target.promise) : pValue
}
if (typeof chainableMethods[target.ctx.type][prop] === 'function') {
return function (...args) {
return promiseWrapper(target.ctx, chainableMethods[target.ctx.type][prop], name, ...args)
}
}
if (typeof regularMethods[target.ctx.type][prop] === 'function') {
return function (...args) {
return target.promise.then((d) => regularMethods[target.ctx.type][prop].call(d, ...args))
}
}
const numValue = parseInt(prop, 10)
if (!isNaN(numValue) && numValue >= 0) {
return promiseWrapper(target.ctx, () => target.promise.then((elArray) => elArray[numValue]), name)
}
return undefined
},
}
)
return proxy
}
function wrapChainableMethods(ctx) {
const wrappedMethods = Object.entries(chainableMethods[ctx.type]).reduce((prev, [name, fn]) => {
prev[name] = function (...args) {
return promiseWrapper(ctx, fn, name, ...args)
}
return prev
}, {})
return wrappedMethods
}
function pBrowserFactory() {
const browserObject = { type: 'browser', isMobile: false }
return Object.assign(browserObject, regularMethods.browser, wrapChainableMethods(browserObject))
}
function pElementFactory(selector, index) {
const ctx = {
type: 'element',
parent: this,
index,
selector: (this.selector || this.type) + '->' + selector,
}
return Object.assign({}, ctx, regularMethods.element, wrapChainableMethods(ctx))
}
const testing = async () => {
const pBrowser = pBrowserFactory()
const chaining = await pBrowser.p$('1').p$('2').p$$('3')[1].p$('4')
console.log('element context saved while chaining', chaining.selector)
console.log('regulat commands have proper context', await chaining.getText())
console.log('parent', chaining.parent.selector, '$$ index', chaining.parent.index)
const savingPromiseContext = pBrowser.p$('11')
savingPromiseContext.p$('1x').p$('2x')
console.log((await savingPromiseContext.p$('22')).selector, (await savingPromiseContext.p$('33')).selector)
const savingResolvedContext = await pBrowser.p$('44')
savingResolvedContext.p$('4x').p$('4x')
console.log((await savingResolvedContext.p$('55')).selector, (await savingResolvedContext.p$('66')).selector)
const noChaining = await pBrowser.p$('77')
console.log('no promise chaining', (await noChaining.p$('88')).selector)
}
testing()
interface PBrowser extends PBrowserMethods, PBrowserOptions {}
interface PBrowserMethods {
p$: (s: string) => ChainablePromiseElement<PElement>
p$$: () => ChainablePromiseArray<ChainablePromiseElement<PElement>>
url: (url: string) => Promise<void>
}
interface PBrowserOptions {
type: 'browser'
isMobile: boolean
parent?: undefined
selector?: undefined
index?: undefined
}
interface PElement extends PElementMethods, PElementOptions {}
interface PElementMethods {
p$: (s: string) => ChainablePromiseElement<PElement>
p$$: (s: string) => ChainablePromiseArray<ChainablePromiseElement<PElement>>
getText: () => Promise<string>
}
interface PElementOptions {
type: 'element'
selector: string
index?: number
parent: PElement | PBrowser
}
interface ChainablePromiseBrowser<T> extends PBrowserMethods, Promise<T> {}
interface ChainablePromiseElement<T> extends PElementMethods, Promise<T> {}
interface ChainablePromiseArray<T> extends Promise<T> {
[n: number]: T
}
// chainable commands exmaple
async function p$(selector: string) {
return pElementFactory.call(this, selector)
}
async function shadow$(selector: string) {
return pElementFactory.call(this, selector)
}
async function p$$(selector: string) {
return [pElementFactory.call(this, selector, 0), pElementFactory.call(this, selector, 1)]
}
// regular commands example
async function getText() {
return `text of "${this.selector}"`
}
async function url(url: string) {
return url
}
const chainableMethods: Record<'browser' | 'element', Record<string, (...args: any[]) => Promise<any>>> = {
element: { p$, p$$, shadow$ },
browser: { p$, p$$ },
}
const regularMethods: Record<'browser' | 'element', Record<string, (...args: any[]) => Promise<any>>> = {
element: { getText },
browser: { url },
}
type ProxyContext = (PElementOptions | PBrowserOptions) & { prevCall?: any }
// proxy magic here
function promiseWrapper<T>(parentContext: ProxyContext, fn: (...args: any[]) => Promise<T>, name: string, ...argz: any[]) {
// previos call should be resolved before the current call
const cmdResultPromise: Promise<any> = parentContext.prevCall
? parentContext.prevCall.then((d: any) => fn.call(d, ...argz))
: fn.call(parentContext, ...argz)
// very important to have a new context
const newCtx = { ...parentContext, type: chainableMethods.element[name] ? 'element' : parentContext.type } as ProxyContext
newCtx.prevCall = cmdResultPromise
const proxy: any = new Proxy(
{ ctx: newCtx, promise: cmdResultPromise },
{
get: function (target, prop: string) {
const pValue = target.promise[prop as keyof Promise<T>]
// we need bind target.promise to then, catch, finally to make Promise work.
if (pValue !== undefined) {
return typeof pValue === 'function' ? pValue.bind(target.promise) : pValue
}
// chain wdio functions
if (typeof chainableMethods[target.ctx.type][prop] === 'function') {
return function (...args: any[]) {
return promiseWrapper(target.ctx, chainableMethods[target.ctx.type][prop], name, ...args)
}
}
// regular fn
if (typeof regularMethods[target.ctx.type][prop] === 'function') {
return function (...args: any[]) {
return target.promise.then((d) => regularMethods[target.ctx.type][prop].call(d, ...args))
}
}
// handle array indicies
const numValue = parseInt(prop, 10)
if (!isNaN(numValue) && numValue >= 0) {
return promiseWrapper(target.ctx, () => target.promise.then((elArray) => (elArray as Array<any>)[numValue]), name)
}
return undefined
},
}
)
return proxy
}
function wrapChainableMethods(ctx: PBrowserOptions | PElementOptions) {
const wrappedMethods = Object.entries(chainableMethods[ctx.type]).reduce((prev, [name, fn]) => {
prev[name] = function (...args: any[]) {
return promiseWrapper(ctx, fn, name, ...args)
}
return prev
}, {} as Record<string, (...args: any[]) => Promise<any>>)
return wrappedMethods
}
function pBrowserFactory() {
const browserObject: PBrowserOptions = { type: 'browser', isMobile: false }
return (Object.assign(browserObject, regularMethods.browser, wrapChainableMethods(browserObject)) as unknown) as PBrowser
}
function pElementFactory(selector: string, index?: number) {
const ctx = {
type: 'element',
parent: this,
index,
selector: (this.selector || this.type) + '->' + selector, // prepend parent selector for debug
} as PElementOptions
return (Object.assign({}, ctx, regularMethods.element, wrapChainableMethods(ctx)) as unknown) as PElement
}
const testing = async () => {
const pBrowser = pBrowserFactory()
const chaining = await pBrowser.p$('1').p$('2').p$$('3')[1].p$('4')
console.log('element context saved while chaining', chaining.selector) // browser->1->2->3->4
console.log('regulat commands have proper context', await chaining.getText()) // text of "browser->1->2->3->4"
console.log('parent', chaining.parent.selector, '$$ index', chaining.parent.index) // 1
const savingPromiseContext = pBrowser.p$('11')
savingPromiseContext.p$('1x').p$('2x') // should not affect next command!
console.log((await savingPromiseContext.p$('22')).selector, (await savingPromiseContext.p$('33')).selector) // browser->11->22 browser->11->33
const savingResolvedContext = await pBrowser.p$('44')
savingResolvedContext.p$('4x').p$('4x') // should not affect next command!
console.log((await savingResolvedContext.p$('55')).selector, (await savingResolvedContext.p$('66')).selector) // browser->44->55 browser->44->66
const noChaining = await pBrowser.p$('77')
console.log('no promise chaining', (await noChaining.p$('88')).selector) // browser->77->88
}
testing()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment