appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
205 lines (188 loc) • 6.94 kB
JavaScript
import _ from 'lodash';
import CssConverter from '../css-converter';
import {errors} from 'appium/driver';
import {util} from 'appium/support';
// we override the xpath search for this first-visible-child selector, which
// looks like /*[@firstVisible="true"]
const MAGIC_FIRST_VIS_CHILD_SEL = /\/\*\[@firstVisible\s*=\s*('|")true\1\]/;
// we likewise override xpath search to provide a shortcut for finding all
// scrollable elements
const MAGIC_SCROLLABLE_SEL = /\/\/\*\[@scrollable\s*=\s*('|")true\1\]/;
const WDA_CLASS_CHAIN_STRATEGY = 'class chain';
export default {
/**
* @this {XCUITestDriver}
*/
async findElOrEls(strategy, selector, mult, context) {
if (this.isWebview()) {
return await this.findWebElementOrElements(strategy, selector, mult, context);
} else {
return await this.findNativeElementOrElements(strategy, selector, mult, context);
}
},
/**
* @this {XCUITestDriver}
* @privateRemarks I'm not sure what these objects are; they aren't `Element`.
* @returns {Promise<any|any[]|undefined>}
*/
async findNativeElementOrElements(strategy, selector, mult, context) {
const initSelector = selector;
let rewroteSelector = false;
if (strategy === '-ios predicate string') {
// WebDriverAgent uses 'predicate string'
strategy = 'predicate string';
} else if (strategy === '-ios class chain') {
// WebDriverAgent uses 'class chain'
strategy = WDA_CLASS_CHAIN_STRATEGY;
} else if (strategy === 'css selector') {
strategy = WDA_CLASS_CHAIN_STRATEGY;
selector = CssConverter.toIosClassChainSelector(selector);
}
// Check if the word 'View' is appended to selector and if it is, strip it out
function stripViewFromSelector(selector) {
// Don't strip it out if it's one of these 4 element types
// (see https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m for reference)
const keepView = [
'XCUIElementTypeScrollView',
'XCUIElementTypeCollectionView',
'XCUIElementTypeTextView',
'XCUIElementTypeWebView',
].includes(selector);
if (!keepView && selector.indexOf('View') === selector.length - 4) {
return selector.substr(0, selector.length - 4);
} else {
return selector;
}
}
if (strategy === 'class name') {
// XCUITest classes have `XCUIElementType` prepended
// first check if there is the old `UIA` prefix
if (selector.startsWith('UIA')) {
selector = selector.substring(3);
}
// now check if we need to add `XCUIElementType`
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') {
// Replace UIA if it comes after a forward slash or is at the beginning of the string
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 await this.doNativeFind(strategy, selector, mult, context);
},
/**
* @this {XCUITestDriver}
*/
async doNativeFind(strategy, selector, mult, context) {
context = util.unwrapElement(context);
let endpoint = `/element${context ? `/${context}/element` : ''}${mult ? 's' : ''}`;
let body = {
using: strategy,
value: selector,
};
/** @type {import('./proxy-helper').AllowedHttpMethod} */
const method = 'POST';
// This is either an array is mult === true
// or an object if mult === false
/** @type {Element[]|undefined} */
let els;
try {
await this.implicitWaitForCondition(async () => {
try {
els = /** @type {Element[]|undefined} */ (
await this.proxyCommand(endpoint, method, body)
);
} catch {
els = [];
}
// we succeed if we get some elements
return !_.isEmpty(els);
});
} catch (err) {
if (err.message && err.message.match(/Condition unmet/)) {
// condition was not met setting res to empty array
els = [];
} else {
throw err;
}
}
if (mult) {
return els;
}
if (_.isEmpty(els)) {
throw new errors.NoSuchElementError();
}
return els;
},
/**
* @this {XCUITestDriver}
*/
async getFirstVisibleChild(mult, context) {
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;
// loop through children via class-chain finds, until we run out of children
// or we find a visible one. This loop looks infinite but its not, because at
// some point the call to doNativeFind will throw with an Element Not Found
// error, when the index gets higher than the number of child elements. This
// is what we want because that error will halt the loop and make it all the
// way to the client.
while (true) {
const strategy = WDA_CLASS_CHAIN_STRATEGY;
const selector = `*[${index}]`;
const nthChild = await this.doNativeFind(strategy, selector, false, context);
const visible = await this.getAttribute('visible', nthChild);
if (visible === 'true') {
this.log.info(`Found first visible child at position ${index}`);
return nthChild;
}
index++;
}
},
};
/**
*
* @param {boolean} mult
* @param {import('@appium/types').AppiumLogger|null} log
* @returns {[string, string]}
*/
function rewriteMagicScrollable(mult, log = null) {
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];
}
/**
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
* @typedef {import('@appium/types').Element} Element
*/