UNPKG

wda-driver

Version:

Facebook WebDriverAgent Node Client Library (not official)

313 lines (284 loc) 8.73 kB
import Session from "./session"; import HTTPClient from './httpclient' import ELEMENTS from '../config/xcuiElementTypes' import sleep from '../util/sleep' import Element from './element' interface SelectorObj { predicate: string // predicate string id: string // raw identifier className: string // attr of className type: string // alias of className name: string // attr for name nameContains: string // attr of name contains nameMatches: string // regex string text: string // alias of name textContains: string // alias of nameContains textMatches: string // alias of nameMatches value: string // attr of value, not used in most times valueContains: string // attr of value contains label: string // attr for label labelContains: string // attr for label contains visible: boolean // is visible enabled: boolean // is enabled classChain: string // string of ios chain query, eg: **/XCUIElementTypeOther[`value BEGINSWITH 'blabla'`] xpath: string // xpath string, a little slow, but works fine timeout: number // maxium wait element time, default 10.0s index: number // index of founded elements parentClassChains: string[] } class Selector { http: HTTPClient session: Session predicate: string id: string className: string name: string namePart: string nameRegex: string value: string valuePart: string label: string labelPart: string visible: boolean enabled: boolean index: number xpath: string classChain: string timeout: number parentClassChains: string[] // WDA use two key to find elements "using", "value" // Examples: // "using" can be on of // "partial link text", "link text" // "name", "id", "accessibility id" // "class name", "class chain", "xpath", "predicate string" // predicate string support many keys // UID, // accessibilityContainer, // accessible, // enabled, // frame, // label, // name, // rect, // type, // value, // visible, // wdAccessibilityContainer, // wdAccessible, // wdEnabled, // wdFrame, // wdLabel, // wdName, // wdRect, // wdType, // wdUID, // wdValue, // wdVisible constructor (httpclient: HTTPClient, session: Session, selectorObj: SelectorObj) { this.http = httpclient this.session = session this.predicate = selectorObj.predicate this.id = selectorObj.id this.className = selectorObj.className || selectorObj.type this.name = this.addEscapeCharacterForQuotePrimeCharacter(selectorObj.name || selectorObj.text) this.namePart = selectorObj.nameContains || selectorObj.textContains this.nameRegex = selectorObj.nameMatches || selectorObj.textMatches this.value = selectorObj.value this.valuePart = selectorObj.valueContains this.label = selectorObj.label this.labelPart = selectorObj.labelContains this.enabled = selectorObj.enabled this.visible = selectorObj.visible this.index = selectorObj.index || 0 this.xpath = this.fixXcuiType(selectorObj.xpath) this.classChain = this.fixXcuiType(selectorObj.classChain) this.timeout = selectorObj.timeout || 10 this.parentClassChains = selectorObj.parentClassChains || [] // some fixtures if (this.className && !this.className.startsWith('XCUIElementType')) { this.className = 'XCUIElementType' + this.className } if (this.nameRegex) { if (!this.nameRegex.startsWith('^') && this.nameRegex.startsWith('.*')) { this.nameRegex = '.*' + this.nameRegex } if (!this.nameRegex.endsWith('$') && !this.nameRegex.endsWith('.*')) { this.nameRegex = this.nameRegex + '.*' } } } /** * Fix for https://github.com/openatx/facebook-wda/issues/33 * * @param v * @return string with properly formated quotes, or non changed text */ private addEscapeCharacterForQuotePrimeCharacter (text: string = '') { return text.replace("'", "\\'").replace('"','\\"') } private fixXcuiType (s: string) { if (!s) return '' const reElement = ELEMENTS.join('|') return s.replace(new RegExp("(" + reElement + ")"), a => 'XCUIElementType' + a) } /** HTTP example response: [ {"ELEMENT": "E2FF5B2A-DBDF-4E67-9179-91609480D80A"}, {"ELEMENT": "597B1A1E-70B9-4CBE-ACAD-40943B0A6034"} ] */ private async wdasearch (using: string, value: string): Promise<any[]> { const elementIds: any[] = [] let { value: data } = await this.http.fetch('post', '/elements', { using , value }) data = typeof data === 'string' ? [] : data data.forEach((d: any) => { elementIds.push(d['ELEMENT']) }) return elementIds } // just return if aleady exists predicate private genClassChain () { if (this.predicate) { return '/XCUIElementTypeAny[`' + this.predicate + '`]' } const qs: string[] = [] if (this.name) { qs.push(`name == '${this.name}'`) } if (this.namePart) { qs.push(`name CONTAINS '${this.namePart}'`) } if (this.nameRegex) { qs.push(`name MATCHES '${this.nameRegex}'`) } if (this.label) { qs.push(`label == '${this.label}'`) } if (this.labelPart) { qs.push(`label CONTAINS '${this.labelPart}'`) } if (this.value) { qs.push(`value == '${this.value}'`) } if (this.valuePart) { qs.push(`value CONTAINS ’${this.valuePart}'`) } if (this.visible !== null && this.visible !== undefined) { qs.push(`visible == ${this.visible.toString()}`) } if (this.enabled !== null && this.enabled !== undefined) { qs.push(`enabled == ${this.enabled.toString()}`) } const predicate = qs.join(' AND ') let chain = '/' + (this.className || 'XCUIElementTypeAny') if (predicate) { chain = chain + '[`' + predicate + '`]' } if (this.index) { chain = chain + `[${this.index}]` } return chain } findElementIds () { if (this.id) return this.wdasearch('id', this.id) if (this.predicate) return this.wdasearch('predicate string', this.predicate) if (this.xpath) return this.wdasearch('xpath', this.xpath) if (this.classChain) return this.wdasearch('class chain', this.classChain) const chain = '**' + this.parentClassChains.join() + this.genClassChain() return this.wdasearch('class chain', chain) } // return Element (list): all the elements async findElements () { const es: any[] = [] const ids = await this.findElementIds() ids.forEach(id => { const e = new Element(this.http.newClient(''), id) es.push(e) }) return es } async count () { const ids = await this.findElementIds() return ids.length } /** * * @param timeout timeout for query element, unit seconds Default 10s * @return Element: UI Element */ async get (timeout: number = this.timeout): Promise<Element> { const startTime = new Date().getTime() while (true) { const elems = await this.findElements() if (elems.length > 0){ return elems[0] } if (startTime + (timeout * 1000) < new Date().getTime()) { break } await sleep(10) } // check alert again const exists = await this.session.alert().exists() if (exists && this.http.alertCallback) { this.http.alertCallback() return await this.get(timeout) } return Promise.reject([]) } // Set element wait timeout setTimeout (s: number) { this.timeout = s return this } child(selectorObj: SelectorObj) { const chain = this.genClassChain() selectorObj['parentClassChains'] = this.parentClassChains.concat([chain]) return new Selector(this.http, this.session, selectorObj) } async exists () { const ids = await this.findElementIds() return ids.length > this.index } /** * Wait element and perform click * @param timeout timeout for wait * @returns bool: if successfully clicked */ async clickExists (timeout: number = 0) { let e: Element try { e = await this.get(timeout) } catch (e) { return false } await e.click() return true } /** * alias of get * @param timeout timeout seconds */ async wait (timeout: number = this.timeout) { return await this.get(timeout) } async waitGone (timeout: number = this.timeout) { const startTime = new Date().getTime() while (startTime + (timeout * 1000) > new Date().getTime()) { if (!await this.exists()) { return true } } return false } } export default Selector export { SelectorObj }