mirror of
https://github.com/appium/appium.git
synced 2026-02-20 10:20:05 -06:00
227 lines
6.7 KiB
JavaScript
227 lines
6.7 KiB
JavaScript
import _ from 'lodash';
|
|
import NODE_MAP from './node-map';
|
|
import {ATTR_MAP, REMOVE_ATTRS} from './attr-map';
|
|
import TRANSFORMS from './transformers';
|
|
|
|
export const ATTR_PREFIX = '@_';
|
|
export const IDX_PATH_PREFIX = `${ATTR_PREFIX}indexPath`;
|
|
export const IDX_PREFIX = `${ATTR_PREFIX}index`;
|
|
|
|
const isAttr = (/** @type {string} */ k) => k.startsWith(ATTR_PREFIX);
|
|
const isNode = (/** @type {string} */ k) => !isAttr(k);
|
|
|
|
/**
|
|
*
|
|
* @param {string} xmlStr
|
|
* @param {string} platform
|
|
* @param {{metadata?: Object, addIndexPath?: boolean}} opts
|
|
* @returns {Promise<{xml: string, unknowns: NodesAndAttributes}>}
|
|
*/
|
|
export async function transformSourceXml(xmlStr, platform, {metadata = {}, addIndexPath = false} = {}) {
|
|
// first thing we want to do is modify the ios source root node, because it doesn't include the
|
|
// necessary index attribute, so we add it if it's not there
|
|
xmlStr = xmlStr.replace('<AppiumAUT>', '<AppiumAUT index="0">');
|
|
const xmlObj = (await singletonXmlParser()).parse(xmlStr);
|
|
const unknowns = transformNode(xmlObj, platform, {
|
|
metadata,
|
|
addIndexPath,
|
|
parentPath: '',
|
|
});
|
|
let transformedXml = (await singletonXmlBuilder()).build(xmlObj).trim();
|
|
transformedXml = `<?xml version="1.0" encoding="UTF-8"?>\n${transformedXml}`;
|
|
return {xml: transformedXml, unknowns};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} nameMap
|
|
* @param {string} name
|
|
* @param {string} platform
|
|
* @returns {string | null}
|
|
*/
|
|
function getUniversalName(nameMap, name, platform) {
|
|
for (const translatedName of Object.keys(nameMap)) {
|
|
const sourceNodes = nameMap[translatedName]?.[platform];
|
|
if (_.isArray(sourceNodes) && sourceNodes.includes(name)) {
|
|
return translatedName;
|
|
}
|
|
if (sourceNodes === name) {
|
|
return translatedName;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} nodeName
|
|
* @param {string} platform
|
|
* @returns {string?}
|
|
*/
|
|
export function getUniversalNodeName(nodeName, platform) {
|
|
return getUniversalName(NODE_MAP, nodeName, platform);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} attrName
|
|
* @param {string} platform
|
|
* @returns {string?}
|
|
*/
|
|
export function getUniversalAttrName(attrName, platform) {
|
|
return getUniversalName(ATTR_MAP, attrName, platform);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} nodeObj
|
|
* @param {string} platform
|
|
* @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts
|
|
* @returns {NodesAndAttributes}
|
|
*/
|
|
export function transformNode(nodeObj, platform, {metadata, addIndexPath, parentPath}) {
|
|
const unknownNodes = [];
|
|
const unknownAttrs = [];
|
|
if (_.isPlainObject(nodeObj)) {
|
|
const keys = Object.keys(nodeObj);
|
|
const childNodeNames = keys.filter(isNode);
|
|
const attrs = keys.filter(isAttr);
|
|
let thisIndexPath = parentPath;
|
|
|
|
if (attrs.length && addIndexPath) {
|
|
if (!attrs.includes(IDX_PREFIX)) {
|
|
throw new Error(`Index path is required but node found with no 'index' attribute`);
|
|
}
|
|
|
|
thisIndexPath = `${parentPath}/${nodeObj[IDX_PREFIX]}`;
|
|
nodeObj[IDX_PATH_PREFIX] = thisIndexPath;
|
|
}
|
|
|
|
TRANSFORMS[platform]?.(nodeObj, metadata);
|
|
unknownAttrs.push(...transformAttrs(nodeObj, attrs, platform));
|
|
const unknowns = transformChildNodes(nodeObj, childNodeNames, platform, {
|
|
metadata,
|
|
addIndexPath,
|
|
parentPath: thisIndexPath,
|
|
});
|
|
unknownAttrs.push(...unknowns.attrs);
|
|
unknownNodes.push(...unknowns.nodes);
|
|
} else if (_.isArray(nodeObj)) {
|
|
for (const childObj of nodeObj) {
|
|
const {nodes, attrs} = transformNode(childObj, platform, {
|
|
metadata,
|
|
addIndexPath,
|
|
parentPath,
|
|
});
|
|
unknownNodes.push(...nodes);
|
|
unknownAttrs.push(...attrs);
|
|
}
|
|
}
|
|
return {
|
|
nodes: _.uniq(unknownNodes),
|
|
attrs: _.uniq(unknownAttrs),
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} nodeObj
|
|
* @param {string[]} childNodeNames
|
|
* @param {string} platform
|
|
* @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts
|
|
* @returns {NodesAndAttributes}
|
|
*/
|
|
export function transformChildNodes(
|
|
nodeObj,
|
|
childNodeNames,
|
|
platform,
|
|
{metadata, addIndexPath, parentPath}
|
|
) {
|
|
const unknownNodes = [];
|
|
const unknownAttrs = [];
|
|
for (const nodeName of childNodeNames) {
|
|
// before modifying the name of this child node, recurse down and modify the subtree
|
|
const {nodes, attrs} = transformNode(nodeObj[nodeName], platform, {
|
|
metadata,
|
|
addIndexPath,
|
|
parentPath,
|
|
});
|
|
unknownNodes.push(...nodes);
|
|
unknownAttrs.push(...attrs);
|
|
|
|
// now translate the node name and replace the subtree with this node
|
|
const universalName = getUniversalNodeName(nodeName, platform);
|
|
if (universalName === null) {
|
|
unknownNodes.push(nodeName);
|
|
continue;
|
|
}
|
|
|
|
// since multiple child node names could map to the same new transformed node name, we can't
|
|
// simply assign nodeObj[universalName] = nodeObj[nodeName]; we need to be sensitive to the
|
|
// situation where the end result is an array of children having the same node name
|
|
if (nodeObj[universalName]) {
|
|
// if we already have a node with the universal name, that means we are mapping a second
|
|
// original node name to the same universal node name, so we just push all its children into
|
|
// the list
|
|
nodeObj[universalName].push(...nodeObj[nodeName]);
|
|
} else {
|
|
nodeObj[universalName] = nodeObj[nodeName];
|
|
}
|
|
delete nodeObj[nodeName];
|
|
}
|
|
return {nodes: unknownNodes, attrs: unknownAttrs};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} nodeObj
|
|
* @param {string[]} attrs
|
|
* @param {string} platform
|
|
* @returns {string[]}
|
|
*/
|
|
export function transformAttrs(nodeObj, attrs, platform) {
|
|
const unknownAttrs = [];
|
|
for (const attr of attrs) {
|
|
const cleanAttr = attr.substring(2);
|
|
if (REMOVE_ATTRS.includes(cleanAttr)) {
|
|
delete nodeObj[attr];
|
|
continue;
|
|
}
|
|
const universalAttr = getUniversalAttrName(cleanAttr, platform);
|
|
if (universalAttr === null) {
|
|
unknownAttrs.push(cleanAttr);
|
|
continue;
|
|
}
|
|
const newAttr = `${ATTR_PREFIX}${universalAttr}`;
|
|
if (newAttr !== attr) {
|
|
nodeObj[newAttr] = nodeObj[attr];
|
|
delete nodeObj[attr];
|
|
}
|
|
}
|
|
return unknownAttrs;
|
|
}
|
|
|
|
const singletonXmlBuilder = _.memoize(async function makeXmlBuilder() {
|
|
const { XMLBuilder } = await import('fast-xml-parser');
|
|
return new XMLBuilder({
|
|
ignoreAttributes: false,
|
|
attributeNamePrefix: ATTR_PREFIX,
|
|
suppressBooleanAttributes: false,
|
|
format: true,
|
|
});
|
|
});
|
|
|
|
const singletonXmlParser = _.memoize(async function makeXmlParser() {
|
|
const { XMLParser } = await import('fast-xml-parser');
|
|
return new XMLParser({
|
|
ignoreAttributes: false,
|
|
ignoreDeclaration: true,
|
|
attributeNamePrefix: ATTR_PREFIX,
|
|
isArray: (name, jPath, isLeafNode, isAttribute) => !isAttribute,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* @typedef {{nodes: string[], attrs: string[]}} NodesAndAttributes
|
|
*/
|