UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

282 lines (266 loc) 7.96 kB
import _ from 'lodash'; import {CssConverter} from '../css-converter'; import {errors} from 'appium/driver'; import {util} from 'appium/support'; import type {Element, AppiumLogger} from '@appium/types'; import type {XCUITestDriver} from '../driver'; import type {AllowedHttpMethod} from './proxy-helper'; /** * Finds elements, delegating to web or native based on context. */ export async function findElOrEls( this: XCUITestDriver, strategy: string, selector: string, mult: true, context?: any, ): Promise<Element[]>; export async function findElOrEls( this: XCUITestDriver, strategy: string, selector: string, mult: false, context?: any, ): Promise<Element>; export async function findElOrEls( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]>; export async function findElOrEls( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]>; export async function findElOrEls( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]> { if (this.isWebview()) { return mult ? await this.findWebElementOrElements(strategy, selector, true, context) : await this.findWebElementOrElements(strategy, selector, false, context); } return mult ? await this.findNativeElementOrElements(strategy, selector, true, context) : await this.findNativeElementOrElements(strategy, selector, false, context); } /** * Finds elements natively with strategy/selector rewriting for WDA. */ export async function findNativeElementOrElements( this: XCUITestDriver, strategy: string, selector: string, mult: true, context?: any, ): Promise<Element[]>; export async function findNativeElementOrElements( this: XCUITestDriver, strategy: string, selector: string, mult: false, context?: any, ): Promise<Element>; export async function findNativeElementOrElements( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]>; export async function findNativeElementOrElements( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]> { const initSelector = selector; let rewroteSelector = false; if (strategy === '-ios predicate string') { strategy = 'predicate string'; } else if (strategy === '-ios class chain') { strategy = WDA_CLASS_CHAIN_STRATEGY; } else if (strategy === 'css selector') { strategy = WDA_CLASS_CHAIN_STRATEGY; selector = CssConverter.toIosClassChainSelector(selector); } if (strategy === 'class name') { if (selector.startsWith('UIA')) { selector = selector.substring(3); } if (!selector.startsWith('XCUIElementType')) { selector = stripViewFromSelector(`XCUIElementType${selector}`); rewroteSelector = true; } } if (strategy === 'xpath' && MAGIC_FIRST_VIS_CHILD_SEL.test(selector)) { return await this.getFirstVisibleChild(mult, context); } else if (strategy === 'xpath' && MAGIC_SCROLLABLE_SEL.test(selector)) { [strategy, selector] = rewriteMagicScrollable(mult, this.log); } else if (strategy === 'xpath') { selector = selector.replace(/(^|\/)(UIA)([^[/]+)/g, (str, g1, _g2, g3) => { rewroteSelector = true; return g1 + stripViewFromSelector(`XCUIElementType${g3}`); }); } if (rewroteSelector) { this.log.info( `Rewrote incoming selector from '${initSelector}' to ` + `'${selector}' to match XCUI type. You should consider ` + `updating your tests to use the new selectors directly`, ); } return mult ? await this.doNativeFind(strategy, selector, true, context) : await this.doNativeFind(strategy, selector, false, context); } /** * Finds elements natively and returns either a single element or an array depending on `mult`. * * Returns an array when `mult` is true; otherwise returns a single element. */ export async function doNativeFind( this: XCUITestDriver, strategy: string, selector: string, mult: true, context?: any, ): Promise<Element[]>; export async function doNativeFind( this: XCUITestDriver, strategy: string, selector: string, mult: false, context?: any, ): Promise<Element>; export async function doNativeFind( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]>; export async function doNativeFind( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]>; export async function doNativeFind( this: XCUITestDriver, strategy: string, selector: string, mult: boolean, context?: any, ): Promise<Element | Element[]> { const ctx = context ? util.unwrapElement(context) : null; const endpoint = `/element${ctx ? `/${ctx}/element` : ''}${mult ? 's' : ''}`; const body = { using: strategy, value: selector, }; const method: AllowedHttpMethod = 'POST'; let els: Element[] | Element = []; const performLookup = async () => { try { els = (await this.proxyCommand(endpoint, method, body)) as Element[] | Element; } catch { els = [] as Element[]; } return !_.isEmpty(els as Element[]); }; try { if (!this.sessionId) { await performLookup(); } else { await this.implicitWaitForCondition(performLookup); } } catch (err: any) { if (err.message?.match(/Condition unmet/)) { els = [] as Element[]; } else { throw err; } } if (mult) { return Array.isArray(els) ? els : [els]; } if (Array.isArray(els)) { if (_.isEmpty(els)) { throw new errors.NoSuchElementError(); } return els[0]; } if (!els) { throw new errors.NoSuchElementError(); } return els; } /** * Finds the first visible child element inside a context. */ export async function getFirstVisibleChild( this: XCUITestDriver, mult: boolean, context: Element | string | null, ): Promise<Element> { this.log.info(`Getting first visible child`); if (mult) { throw new Error('Cannot get multiple first visible children!'); } if (!context) { throw new Error('Cannot get first visible child without a context element'); } let index = 1; while (true) { const strategy = WDA_CLASS_CHAIN_STRATEGY; const selector = `*[${index}]`; const nthChild = (await this.doNativeFind(strategy, selector, false, context)) as Element; const visible = await this.getAttribute('visible', nthChild); if (visible === 'true') { this.log.info(`Found first visible child at position ${index}`); return nthChild; } index++; } } const MAGIC_FIRST_VIS_CHILD_SEL = /\/\*\[@firstVisible\s*=\s*('|")true\1\]/; const MAGIC_SCROLLABLE_SEL = /\/\/\*\[@scrollable\s*=\s*('|")true\1\]/; const WDA_CLASS_CHAIN_STRATEGY = 'class chain'; function stripViewFromSelector(selector: string): string { const keepView = [ 'XCUIElementTypeScrollView', 'XCUIElementTypeCollectionView', 'XCUIElementTypeTextView', 'XCUIElementTypeWebView', ].includes(selector); if (!keepView && selector.indexOf('View') === selector.length - 4) { return selector.substring(0, selector.length - 4); } return selector; } function rewriteMagicScrollable(mult: boolean, log: AppiumLogger | null = null): [string, string] { const pred = ['ScrollView', 'Table', 'CollectionView', 'WebView'] .map((t) => `type == "XCUIElementType${t}"`) .join(' OR '); const strategy = WDA_CLASS_CHAIN_STRATEGY; let selector = '**/*[`' + pred + '`]'; if (!mult) { selector += '[1]'; } log?.info( 'Rewrote request for scrollable descendants to class chain ' + `format with selector '${selector}'`, ); return [strategy, selector]; }