Last active
February 4, 2022 01:15
-
-
Save mgrybyk/a0714cc9d3d2df75ac3e5907d2f6fb46 to your computer and use it in GitHub Desktop.
WebdriverIO - new async api
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
// 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() |
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
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