@appium/universal-xml-plugin 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/build/lib/attr-map.d.ts +3 -57
  3. package/build/lib/attr-map.d.ts.map +1 -1
  4. package/build/lib/attr-map.js +2 -5
  5. package/build/lib/attr-map.js.map +1 -1
  6. package/build/lib/index.d.ts +4 -3
  7. package/build/lib/index.d.ts.map +1 -1
  8. package/build/lib/index.js +20 -5
  9. package/build/lib/index.js.map +1 -1
  10. package/build/lib/node-map.d.ts +3 -199
  11. package/build/lib/node-map.d.ts.map +1 -1
  12. package/build/lib/node-map.js +2 -9
  13. package/build/lib/node-map.js.map +1 -1
  14. package/build/lib/plugin.d.ts +7 -6
  15. package/build/lib/plugin.d.ts.map +1 -1
  16. package/build/lib/plugin.js +16 -18
  17. package/build/lib/plugin.js.map +1 -1
  18. package/build/lib/source.d.ts +41 -47
  19. package/build/lib/source.d.ts.map +1 -1
  20. package/build/lib/source.js +104 -47
  21. package/build/lib/source.js.map +1 -1
  22. package/build/lib/transformers.d.ts +3 -7
  23. package/build/lib/transformers.d.ts.map +1 -1
  24. package/build/lib/transformers.js +9 -10
  25. package/build/lib/transformers.js.map +1 -1
  26. package/build/lib/types.d.ts +29 -0
  27. package/build/lib/types.d.ts.map +1 -0
  28. package/build/lib/types.js +3 -0
  29. package/build/lib/types.js.map +1 -0
  30. package/build/lib/xpath.d.ts +16 -7
  31. package/build/lib/xpath.d.ts.map +1 -1
  32. package/build/lib/xpath.js +19 -16
  33. package/build/lib/xpath.js.map +1 -1
  34. package/lib/{attr-map.js → attr-map.ts} +5 -5
  35. package/lib/{index.js → index.ts} +22 -4
  36. package/lib/{node-map.js → node-map.ts} +5 -1
  37. package/lib/plugin.ts +119 -0
  38. package/lib/source.ts +265 -0
  39. package/lib/{transformers.js → transformers.ts} +9 -12
  40. package/lib/types.ts +33 -0
  41. package/lib/{xpath.js → xpath.ts} +24 -20
  42. package/package.json +7 -7
  43. package/tsconfig.json +3 -2
  44. package/build/lib/logger.d.ts +0 -3
  45. package/build/lib/logger.d.ts.map +0 -1
  46. package/build/lib/logger.js +0 -6
  47. package/build/lib/logger.js.map +0 -1
  48. package/index.js +0 -7
  49. package/lib/logger.js +0 -3
  50. package/lib/plugin.js +0 -81
  51. package/lib/source.js +0 -226
@@ -2,22 +2,23 @@ import {select as xpathQuery} from 'xpath';
2
2
  import {DOMParser, MIME_TYPE} from '@xmldom/xmldom';
3
3
  import _ from 'lodash';
4
4
 
5
- export function runQuery(query, xmlStr) {
5
+ export function runQuery(query: string, xmlStr: string): any[] {
6
6
  const dom = new DOMParser().parseFromString(xmlStr, MIME_TYPE.XML_TEXT);
7
7
  // @ts-expect-error Missing Node properties are not needed.
8
8
  // https://github.com/xmldom/xmldom/issues/724
9
9
  const nodes = xpathQuery(query, dom);
10
- return nodes;
10
+ return nodes as any[];
11
11
  }
12
12
 
13
13
  /**
14
+ * Transforms an XPath query to work with the original platform-specific XML
14
15
  *
15
- * @param {string} query
16
- * @param {string} xmlStr
17
- * @param {boolean} multiple
18
- * @returns {string|null}
16
+ * @param query - The XPath query to transform
17
+ * @param xmlStr - The transformed XML string
18
+ * @param multiple - Whether to return multiple matches
19
+ * @returns The transformed query string or null if no matches found
19
20
  */
20
- export function transformQuery(query, xmlStr, multiple) {
21
+ export function transformQuery(query: string, xmlStr: string, multiple: boolean): string | null {
21
22
  const nodes = runQuery(query, xmlStr);
22
23
  if (!_.isArray(nodes)) {
23
24
  return null;
@@ -26,9 +27,9 @@ export function transformQuery(query, xmlStr, multiple) {
26
27
  const newQueries = nodes.map((node) => {
27
28
  const indexPath = getNodeAttrVal(node, 'indexPath');
28
29
  // at this point indexPath will look like /0/0/1/1/0/1/0/2
29
- let newQuery = indexPath
30
+ const newQuery = indexPath
30
31
  .substring(1) // remove leading / so we can split
31
- .split('/') // split into idnexes
32
+ .split('/') // split into indexes
32
33
  .map((indexStr) => {
33
34
  // map to xpath node indexes (1-based)
34
35
  const xpathIndex = parseInt(indexStr, 10) + 1;
@@ -40,21 +41,24 @@ export function transformQuery(query, xmlStr, multiple) {
40
41
  return `/${newQuery}`;
41
42
  });
42
43
 
43
- let newSelector = null;
44
- if (newQueries.length) {
45
- if (multiple) {
46
- newSelector = newQueries.join(' | ');
47
- } else {
48
- newSelector = newQueries[0];
49
- }
44
+ if (newQueries.length === 0) {
45
+ return null;
50
46
  }
51
- return newSelector;
47
+ return multiple ? newQueries.join(' | ') : newQueries[0];
52
48
  }
53
49
 
54
- export function getNodeAttrVal(node, attr) {
55
- const attrObjs = Object.values(node.attributes).filter((obj) => obj.name === attr);
50
+ /**
51
+ * Gets the value of a node attribute
52
+ *
53
+ * @param node - The XML node
54
+ * @param attr - The attribute name
55
+ * @returns The attribute value
56
+ * @throws {Error} If the attribute doesn't exist
57
+ */
58
+ export function getNodeAttrVal(node: any, attr: string): string {
59
+ const attrObjs = Object.values(node.attributes || {}).filter((obj: any) => obj.name === attr);
56
60
  if (!attrObjs.length) {
57
61
  throw new Error(`Tried to retrieve a node attribute '${attr}' but the node didn't have it`);
58
62
  }
59
- return attrObjs[0].value;
63
+ return (attrObjs[0] as any).value;
60
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appium/universal-xml-plugin",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Appium plugin for making XML source and XPath queries the same across iOS and Android",
5
5
  "keywords": [
6
6
  "automation",
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "license": "Apache-2.0",
25
25
  "author": "https://github.com/appium",
26
- "types": "./build/lib/plugin.d.ts",
26
+ "main": "./build/lib/index.js",
27
+ "types": "./build/lib/index.d.ts",
27
28
  "files": [
28
- "index.js",
29
29
  "build",
30
30
  "lib",
31
31
  "tsconfig.json",
@@ -34,12 +34,12 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "test": "npm run test:unit",
37
- "test:smoke": "node -e 'require(\"./index.js\")'",
38
- "test:unit": "mocha \"./test/unit/**/*.spec.js\""
37
+ "test:smoke": "node ./build/lib/index.js --smoke-test",
38
+ "test:unit": "mocha \"./test/unit/**/*.spec.*ts\""
39
39
  },
40
40
  "dependencies": {
41
41
  "@xmldom/xmldom": "0.9.8",
42
- "fast-xml-parser": "5.3.3",
42
+ "fast-xml-parser": "5.4.2",
43
43
  "lodash": "4.17.23",
44
44
  "xpath": "0.0.34"
45
45
  },
@@ -57,5 +57,5 @@
57
57
  "publishConfig": {
58
58
  "access": "public"
59
59
  },
60
- "gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8"
60
+ "gitHead": "980a121804ae006db879fb6860f627ac36174c15"
61
61
  }
package/tsconfig.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "outDir": "build",
4
- "checkJs": true
4
+ "checkJs": true,
5
+ "types": ["mocha", "chai", "chai-as-promised"]
5
6
  },
6
7
  "extends": "@appium/tsconfig/tsconfig.plugin.json",
7
- "include": ["lib"]
8
+ "include": ["lib", "test"]
8
9
  }
@@ -1,3 +0,0 @@
1
- export default log;
2
- declare const log: import("@appium/types").AppiumLogger;
3
- //# sourceMappingURL=logger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../lib/logger.js"],"names":[],"mappings":";AACA,wDAAmD"}
@@ -1,6 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const support_1 = require("appium/support");
4
- const log = support_1.logger.getLogger('UniversalXMLPlugin');
5
- exports.default = log;
6
- //# sourceMappingURL=logger.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../lib/logger.js"],"names":[],"mappings":";;AAAA,4CAAsC;AACtC,MAAM,GAAG,GAAG,gBAAM,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;AACnD,kBAAe,GAAG,CAAC"}
package/index.js DELETED
@@ -1,7 +0,0 @@
1
- const {main, UniversalXMLPlugin} = require('./build/lib');
2
-
3
- if (require.main === module) {
4
- (async () => await main())();
5
- }
6
-
7
- module.exports = {UniversalXMLPlugin};
package/lib/logger.js DELETED
@@ -1,3 +0,0 @@
1
- import {logger} from 'appium/support';
2
- const log = logger.getLogger('UniversalXMLPlugin');
3
- export default log;
package/lib/plugin.js DELETED
@@ -1,81 +0,0 @@
1
- import {BasePlugin} from 'appium/plugin';
2
- import {errors} from 'appium/driver';
3
- import {transformSourceXml} from './source';
4
- import {transformQuery} from './xpath';
5
- import log from './logger';
6
-
7
- export default class UniversalXMLPlugin extends BasePlugin {
8
- async getPageSource(next, driver, sessId, addIndexPath = false) {
9
- const source = next ? await next() : await driver.getPageSource();
10
- const metadata = {};
11
- const {platformName} = driver.caps;
12
- if (platformName.toLowerCase() === 'android') {
13
- metadata.appPackage = driver.opts.appPackage;
14
- }
15
- const {xml, unknowns} = await transformSourceXml(source, platformName.toLowerCase(), {
16
- metadata,
17
- addIndexPath,
18
- });
19
- if (unknowns.nodes.length) {
20
- log.warn(
21
- `The XML mapper found ${unknowns.nodes.length} node(s) / ` +
22
- `tag name(s) that it didn't know about. These should be ` +
23
- `reported to improve the quality of the plugin: ` +
24
- unknowns.nodes.join(', '),
25
- );
26
- }
27
- if (unknowns.attrs.length) {
28
- log.warn(
29
- `The XML mapper found ${unknowns.attrs.length} attributes ` +
30
- `that it didn't know about. These should be reported to ` +
31
- `improve the quality of the plugin: ` +
32
- unknowns.attrs.join(', '),
33
- );
34
- }
35
- return xml;
36
- }
37
-
38
- async findElement(...args) {
39
- return await this._find(false, ...args);
40
- }
41
-
42
- async findElements(...args) {
43
- return await this._find(true, ...args);
44
- }
45
-
46
- async _find(multiple, next, driver, strategy, selector) {
47
- const {platformName} = driver.caps;
48
- if (strategy.toLowerCase() !== 'xpath' || (await driver.getCurrentContext()) !== 'NATIVE_APP') {
49
- return await next();
50
- }
51
- const xml = await this.getPageSource(null, driver, null, true);
52
- let newSelector = transformQuery(selector, xml, multiple);
53
-
54
- // if the selector was not able to be transformed, that means no elements were found that
55
- // matched, so do the appropriate thing based on element vs elements
56
- if (newSelector === null) {
57
- log.warn(
58
- `Selector was not able to be translated to underlying XML. Either the requested ` +
59
- `element does not exist or there was an error in translation`,
60
- );
61
- if (multiple) {
62
- return [];
63
- }
64
- throw new errors.NoSuchElementError();
65
- }
66
-
67
- if (platformName.toLowerCase() === 'ios') {
68
- // with the XCUITest driver, the <AppiumAUT> wrapper element is present in the source but is
69
- // not present in the source considered by WDA, so our index path based xpath queries will
70
- // not work with WDA as-is. We need to remove the first path segment.
71
- newSelector = newSelector.replace(/^\/\*\[1\]/, '');
72
- }
73
- log.info(`Selector was translated to: ${newSelector}`);
74
-
75
- // otherwise just run the transformed query!
76
- const finder = multiple ? 'findElements' : 'findElement';
77
- return await driver[finder](strategy, newSelector);
78
- }
79
- }
80
-
81
- export {UniversalXMLPlugin};
package/lib/source.js DELETED
@@ -1,226 +0,0 @@
1
- import _ from 'lodash';
2
- import NODE_MAP from './node-map';
3
- import {ATTR_MAP, REMOVE_ATTRS} from './attr-map';
4
- import TRANSFORMS from './transformers';
5
-
6
- export const ATTR_PREFIX = '@_';
7
- export const IDX_PATH_PREFIX = `${ATTR_PREFIX}indexPath`;
8
- export const IDX_PREFIX = `${ATTR_PREFIX}index`;
9
-
10
- const isAttr = (/** @type {string} */ k) => k.startsWith(ATTR_PREFIX);
11
- const isNode = (/** @type {string} */ k) => !isAttr(k);
12
-
13
- /**
14
- *
15
- * @param {string} xmlStr
16
- * @param {string} platform
17
- * @param {{metadata?: Object, addIndexPath?: boolean}} opts
18
- * @returns {Promise<{xml: string, unknowns: NodesAndAttributes}>}
19
- */
20
- export async function transformSourceXml(xmlStr, platform, {metadata = {}, addIndexPath = false} = {}) {
21
- // first thing we want to do is modify the ios source root node, because it doesn't include the
22
- // necessary index attribute, so we add it if it's not there
23
- xmlStr = xmlStr.replace('<AppiumAUT>', '<AppiumAUT index="0">');
24
- const xmlObj = (await singletonXmlParser()).parse(xmlStr);
25
- const unknowns = transformNode(xmlObj, platform, {
26
- metadata,
27
- addIndexPath,
28
- parentPath: '',
29
- });
30
- let transformedXml = (await singletonXmlBuilder()).build(xmlObj).trim();
31
- transformedXml = `<?xml version="1.0" encoding="UTF-8"?>\n${transformedXml}`;
32
- return {xml: transformedXml, unknowns};
33
- }
34
-
35
- /**
36
- *
37
- * @param {Object} nameMap
38
- * @param {string} name
39
- * @param {string} platform
40
- * @returns {string | null}
41
- */
42
- function getUniversalName(nameMap, name, platform) {
43
- for (const translatedName of Object.keys(nameMap)) {
44
- const sourceNodes = nameMap[translatedName]?.[platform];
45
- if (_.isArray(sourceNodes) && sourceNodes.includes(name)) {
46
- return translatedName;
47
- }
48
- if (sourceNodes === name) {
49
- return translatedName;
50
- }
51
- }
52
- return null;
53
- }
54
-
55
- /**
56
- *
57
- * @param {any} nodeName
58
- * @param {string} platform
59
- * @returns {string?}
60
- */
61
- export function getUniversalNodeName(nodeName, platform) {
62
- return getUniversalName(NODE_MAP, nodeName, platform);
63
- }
64
-
65
- /**
66
- *
67
- * @param {string} attrName
68
- * @param {string} platform
69
- * @returns {string?}
70
- */
71
- export function getUniversalAttrName(attrName, platform) {
72
- return getUniversalName(ATTR_MAP, attrName, platform);
73
- }
74
-
75
- /**
76
- *
77
- * @param {any} nodeObj
78
- * @param {string} platform
79
- * @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts
80
- * @returns {NodesAndAttributes}
81
- */
82
- export function transformNode(nodeObj, platform, {metadata, addIndexPath, parentPath}) {
83
- const unknownNodes = [];
84
- const unknownAttrs = [];
85
- if (_.isPlainObject(nodeObj)) {
86
- const keys = Object.keys(nodeObj);
87
- const childNodeNames = keys.filter(isNode);
88
- const attrs = keys.filter(isAttr);
89
- let thisIndexPath = parentPath;
90
-
91
- if (attrs.length && addIndexPath) {
92
- if (!attrs.includes(IDX_PREFIX)) {
93
- throw new Error(`Index path is required but node found with no 'index' attribute`);
94
- }
95
-
96
- thisIndexPath = `${parentPath}/${nodeObj[IDX_PREFIX]}`;
97
- nodeObj[IDX_PATH_PREFIX] = thisIndexPath;
98
- }
99
-
100
- TRANSFORMS[platform]?.(nodeObj, metadata);
101
- unknownAttrs.push(...transformAttrs(nodeObj, attrs, platform));
102
- const unknowns = transformChildNodes(nodeObj, childNodeNames, platform, {
103
- metadata,
104
- addIndexPath,
105
- parentPath: thisIndexPath,
106
- });
107
- unknownAttrs.push(...unknowns.attrs);
108
- unknownNodes.push(...unknowns.nodes);
109
- } else if (_.isArray(nodeObj)) {
110
- for (const childObj of nodeObj) {
111
- const {nodes, attrs} = transformNode(childObj, platform, {
112
- metadata,
113
- addIndexPath,
114
- parentPath,
115
- });
116
- unknownNodes.push(...nodes);
117
- unknownAttrs.push(...attrs);
118
- }
119
- }
120
- return {
121
- nodes: _.uniq(unknownNodes),
122
- attrs: _.uniq(unknownAttrs),
123
- };
124
- }
125
-
126
- /**
127
- *
128
- * @param {any} nodeObj
129
- * @param {string[]} childNodeNames
130
- * @param {string} platform
131
- * @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts
132
- * @returns {NodesAndAttributes}
133
- */
134
- export function transformChildNodes(
135
- nodeObj,
136
- childNodeNames,
137
- platform,
138
- {metadata, addIndexPath, parentPath}
139
- ) {
140
- const unknownNodes = [];
141
- const unknownAttrs = [];
142
- for (const nodeName of childNodeNames) {
143
- // before modifying the name of this child node, recurse down and modify the subtree
144
- const {nodes, attrs} = transformNode(nodeObj[nodeName], platform, {
145
- metadata,
146
- addIndexPath,
147
- parentPath,
148
- });
149
- unknownNodes.push(...nodes);
150
- unknownAttrs.push(...attrs);
151
-
152
- // now translate the node name and replace the subtree with this node
153
- const universalName = getUniversalNodeName(nodeName, platform);
154
- if (universalName === null) {
155
- unknownNodes.push(nodeName);
156
- continue;
157
- }
158
-
159
- // since multiple child node names could map to the same new transformed node name, we can't
160
- // simply assign nodeObj[universalName] = nodeObj[nodeName]; we need to be sensitive to the
161
- // situation where the end result is an array of children having the same node name
162
- if (nodeObj[universalName]) {
163
- // if we already have a node with the universal name, that means we are mapping a second
164
- // original node name to the same universal node name, so we just push all its children into
165
- // the list
166
- nodeObj[universalName].push(...nodeObj[nodeName]);
167
- } else {
168
- nodeObj[universalName] = nodeObj[nodeName];
169
- }
170
- delete nodeObj[nodeName];
171
- }
172
- return {nodes: unknownNodes, attrs: unknownAttrs};
173
- }
174
-
175
- /**
176
- *
177
- * @param {any} nodeObj
178
- * @param {string[]} attrs
179
- * @param {string} platform
180
- * @returns {string[]}
181
- */
182
- export function transformAttrs(nodeObj, attrs, platform) {
183
- const unknownAttrs = [];
184
- for (const attr of attrs) {
185
- const cleanAttr = attr.substring(2);
186
- if (REMOVE_ATTRS.includes(cleanAttr)) {
187
- delete nodeObj[attr];
188
- continue;
189
- }
190
- const universalAttr = getUniversalAttrName(cleanAttr, platform);
191
- if (universalAttr === null) {
192
- unknownAttrs.push(cleanAttr);
193
- continue;
194
- }
195
- const newAttr = `${ATTR_PREFIX}${universalAttr}`;
196
- if (newAttr !== attr) {
197
- nodeObj[newAttr] = nodeObj[attr];
198
- delete nodeObj[attr];
199
- }
200
- }
201
- return unknownAttrs;
202
- }
203
-
204
- const singletonXmlBuilder = _.memoize(async function makeXmlBuilder() {
205
- const { XMLBuilder } = await import('fast-xml-parser');
206
- return new XMLBuilder({
207
- ignoreAttributes: false,
208
- attributeNamePrefix: ATTR_PREFIX,
209
- suppressBooleanAttributes: false,
210
- format: true,
211
- });
212
- });
213
-
214
- const singletonXmlParser = _.memoize(async function makeXmlParser() {
215
- const { XMLParser } = await import('fast-xml-parser');
216
- return new XMLParser({
217
- ignoreAttributes: false,
218
- ignoreDeclaration: true,
219
- attributeNamePrefix: ATTR_PREFIX,
220
- isArray: (name, jPath, isLeafNode, isAttribute) => !isAttribute,
221
- });
222
- });
223
-
224
- /**
225
- * @typedef {{nodes: string[], attrs: string[]}} NodesAndAttributes
226
- */