import type {Element, ExternalDriver, NextPluginCallback} from '@appium/types'; import {errors} from 'appium/driver.js'; import {BasePlugin} from 'appium/plugin.js'; import {transformSourceXml} from './source.js'; import type {TransformMetadata} from './types.js'; import {transformQuery} from './xpath.js'; export class UniversalXMLPlugin extends BasePlugin { async getPageSource( next: NextPluginCallback | null, driver: ExternalDriver, sessId?: any, addIndexPath: boolean = false, ): Promise { void sessId; const source = (next ? await next() : await driver.getPageSource()) as string; const metadata: TransformMetadata = {}; const platformName = getPlatformName(driver); if (platformName.toLowerCase() === 'android') { metadata.appPackage = (driver.opts as Record)?.appPackage as string; } const {xml, unknowns} = await transformSourceXml(source, platformName.toLowerCase(), { metadata, addIndexPath, }); if (unknowns.nodes.length) { this.log.warn( `The XML mapper found ${unknowns.nodes.length} node(s) / ` + `tag name(s) that it didn't know about. These should be ` + `reported to improve the quality of the plugin: ` + unknowns.nodes.join(', '), ); } if (unknowns.attrs.length) { this.log.warn( `The XML mapper found ${unknowns.attrs.length} attributes ` + `that it didn't know about. These should be reported to ` + `improve the quality of the plugin: ` + unknowns.attrs.join(', '), ); } return xml; } async findElement( next: NextPluginCallback, driver: ExternalDriver, strategy: string, selector: string, ): Promise { return (await this._find(false, next, driver, strategy, selector)) as Element; } async findElements( next: NextPluginCallback, driver: ExternalDriver, strategy: string, selector: string, ): Promise { return (await this._find(true, next, driver, strategy, selector)) as Element[]; } private async _find( multiple: false, next: NextPluginCallback, driver: ExternalDriver, strategy: string, selector: string, ): Promise; private async _find( multiple: true, next: NextPluginCallback, driver: ExternalDriver, strategy: string, selector: string, ): Promise; private async _find( multiple: boolean, next: NextPluginCallback, driver: ExternalDriver, strategy: string, selector: string, ): Promise { if ( strategy.toLowerCase() !== 'xpath' || !driver.getCurrentContext || (await driver.getCurrentContext()) !== 'NATIVE_APP' ) { return (await next()) as Element | Element[]; } const xml = await this.getPageSource(null, driver, null, true); const newSelector = transformQuery(selector, xml, multiple); // if the selector was not able to be transformed, that means no elements were found that // matched, so do the appropriate thing based on element vs elements if (newSelector === null) { this.log.warn( `Selector was not able to be translated to underlying XML. Either the requested ` + `element does not exist or there was an error in translation`, ); if (multiple) { return []; } throw new errors.NoSuchElementError(); } this.log.info(`Selector was translated to: ${newSelector}`); // otherwise just run the transformed query! const finder = multiple ? 'findElements' : 'findElement'; return (await driver[finder](strategy, newSelector)) as Element | Element[]; } } function getPlatformName(driver: ExternalDriver): string { return ((driver.caps as Record)?.platformName as string) || ''; }