@appium/universal-xml-plugin 2.0.3 → 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 (50) hide show
  1. package/build/lib/attr-map.d.ts +3 -57
  2. package/build/lib/attr-map.d.ts.map +1 -1
  3. package/build/lib/attr-map.js +2 -5
  4. package/build/lib/attr-map.js.map +1 -1
  5. package/build/lib/index.d.ts +4 -3
  6. package/build/lib/index.d.ts.map +1 -1
  7. package/build/lib/index.js +20 -5
  8. package/build/lib/index.js.map +1 -1
  9. package/build/lib/node-map.d.ts +3 -199
  10. package/build/lib/node-map.d.ts.map +1 -1
  11. package/build/lib/node-map.js +2 -9
  12. package/build/lib/node-map.js.map +1 -1
  13. package/build/lib/plugin.d.ts +7 -6
  14. package/build/lib/plugin.d.ts.map +1 -1
  15. package/build/lib/plugin.js +16 -18
  16. package/build/lib/plugin.js.map +1 -1
  17. package/build/lib/source.d.ts +41 -47
  18. package/build/lib/source.d.ts.map +1 -1
  19. package/build/lib/source.js +104 -47
  20. package/build/lib/source.js.map +1 -1
  21. package/build/lib/transformers.d.ts +3 -7
  22. package/build/lib/transformers.d.ts.map +1 -1
  23. package/build/lib/transformers.js +9 -10
  24. package/build/lib/transformers.js.map +1 -1
  25. package/build/lib/types.d.ts +29 -0
  26. package/build/lib/types.d.ts.map +1 -0
  27. package/build/lib/types.js +3 -0
  28. package/build/lib/types.js.map +1 -0
  29. package/build/lib/xpath.d.ts +16 -7
  30. package/build/lib/xpath.d.ts.map +1 -1
  31. package/build/lib/xpath.js +19 -16
  32. package/build/lib/xpath.js.map +1 -1
  33. package/lib/{attr-map.js → attr-map.ts} +5 -5
  34. package/lib/{index.js → index.ts} +22 -4
  35. package/lib/{node-map.js → node-map.ts} +5 -1
  36. package/lib/plugin.ts +119 -0
  37. package/lib/source.ts +265 -0
  38. package/lib/{transformers.js → transformers.ts} +9 -12
  39. package/lib/types.ts +33 -0
  40. package/lib/{xpath.js → xpath.ts} +24 -20
  41. package/package.json +10 -10
  42. package/tsconfig.json +3 -2
  43. package/build/lib/logger.d.ts +0 -3
  44. package/build/lib/logger.d.ts.map +0 -1
  45. package/build/lib/logger.js +0 -6
  46. package/build/lib/logger.js.map +0 -1
  47. package/index.js +0 -7
  48. package/lib/logger.js +0 -3
  49. package/lib/plugin.js +0 -81
  50. package/lib/source.js +0 -226
@@ -1,5 +1,7 @@
1
1
  // uses the same format as NODE_MAP in node-map.js
2
- const ATTR_MAP = {
2
+ import type {UniversalNameMap} from './types';
3
+
4
+ export const ATTR_MAP: UniversalNameMap = {
3
5
  x: {ios: 'x', android: 'x'},
4
6
  y: {ios: 'y', android: 'y'},
5
7
  width: {ios: 'width', android: 'width'},
@@ -13,7 +15,7 @@ const ATTR_MAP = {
13
15
  };
14
16
 
15
17
  // these attributes shouldn't be mapped and should instead just be removed
16
- const REMOVE_ATTRS = [
18
+ export const REMOVE_ATTRS = [
17
19
  'index',
18
20
  'type',
19
21
  'package',
@@ -30,6 +32,4 @@ const REMOVE_ATTRS = [
30
32
  'selected',
31
33
  'bounds',
32
34
  'rotation',
33
- ];
34
-
35
- export {ATTR_MAP, REMOVE_ATTRS};
35
+ ] as const;
@@ -1,11 +1,25 @@
1
- import {UniversalXMLPlugin} from './plugin';
2
- import {transformSourceXml} from './source';
1
+ export {UniversalXMLPlugin} from './plugin';
2
+ export {transformSourceXml} from './source';
3
3
  import fs from 'node:fs/promises';
4
+ import {transformSourceXml} from './source';
5
+ import {UniversalXMLPlugin} from './plugin';
6
+
4
7
  export default UniversalXMLPlugin;
5
- export {UniversalXMLPlugin};
6
8
 
7
- export async function main() {
9
+ export async function main(): Promise<void> {
8
10
  const [, , xmlDataPath, platform, optsJson] = process.argv;
11
+
12
+ // Handle smoke test flag
13
+ if (xmlDataPath === '--smoke-test') {
14
+ // Module loaded successfully, exit with code 0
15
+ process.exit(0);
16
+ }
17
+
18
+ if (!xmlDataPath || !platform) {
19
+ console.error('Usage: node index.js <xmlDataPath> <platform> [optsJson]'); // eslint-disable-line no-console
20
+ process.exit(1);
21
+ }
22
+
9
23
  const xmlData = await fs.readFile(xmlDataPath, 'utf8');
10
24
  const opts = optsJson ? JSON.parse(optsJson) : {};
11
25
  const {xml, unknowns} = await transformSourceXml(xmlData, platform, opts);
@@ -14,3 +28,7 @@ export async function main() {
14
28
  console.error(unknowns); // eslint-disable-line no-console
15
29
  }
16
30
  }
31
+
32
+ if (require.main === module) {
33
+ (async () => await main())();
34
+ }
@@ -6,7 +6,9 @@
6
6
  * array of strings. This means that there can be a many-to-one relationship between
7
7
  * platform-specific node names and universal node names.
8
8
  */
9
- export default {
9
+ import type {UniversalNameMap} from './types';
10
+
11
+ const NODE_MAP: UniversalNameMap = {
10
12
  Alert: {
11
13
  ios: 'XCUIElementTypeAlert',
12
14
  android: 'android.widget.Toast',
@@ -222,3 +224,5 @@ export default {
222
224
  ios: 'XCUIElementTypeWindow',
223
225
  },
224
226
  };
227
+
228
+ export default NODE_MAP;
package/lib/plugin.ts ADDED
@@ -0,0 +1,119 @@
1
+ import {BasePlugin} from 'appium/plugin';
2
+ import {errors} from 'appium/driver';
3
+ import {transformSourceXml} from './source';
4
+ import {transformQuery} from './xpath';
5
+ import type {ExternalDriver, NextPluginCallback, Element} from '@appium/types';
6
+ import type {TransformMetadata} from './types';
7
+
8
+ export class UniversalXMLPlugin extends BasePlugin {
9
+ async getPageSource(
10
+ next: NextPluginCallback | null,
11
+ driver: ExternalDriver,
12
+ sessId?: any,
13
+ addIndexPath: boolean = false
14
+ ): Promise<string> {
15
+ const source = (next ? await next() : await driver.getPageSource()) as string;
16
+ const metadata: TransformMetadata = {};
17
+ const platformName = getPlatformName(driver);
18
+ if (platformName.toLowerCase() === 'android') {
19
+ metadata.appPackage = (driver.opts as any)?.appPackage;
20
+ }
21
+ const {xml, unknowns} = await transformSourceXml(source, platformName.toLowerCase(), {
22
+ metadata,
23
+ addIndexPath,
24
+ });
25
+ if (unknowns.nodes.length) {
26
+ this.log.warn(
27
+ `The XML mapper found ${unknowns.nodes.length} node(s) / ` +
28
+ `tag name(s) that it didn't know about. These should be ` +
29
+ `reported to improve the quality of the plugin: ` +
30
+ unknowns.nodes.join(', ')
31
+ );
32
+ }
33
+ if (unknowns.attrs.length) {
34
+ this.log.warn(
35
+ `The XML mapper found ${unknowns.attrs.length} attributes ` +
36
+ `that it didn't know about. These should be reported to ` +
37
+ `improve the quality of the plugin: ` +
38
+ unknowns.attrs.join(', ')
39
+ );
40
+ }
41
+ return xml;
42
+ }
43
+
44
+ async findElement(
45
+ next: NextPluginCallback,
46
+ driver: ExternalDriver,
47
+ strategy: string,
48
+ selector: string
49
+ ): Promise<Element> {
50
+ return (await this._find(false, next, driver, strategy, selector)) as Element;
51
+ }
52
+
53
+ async findElements(
54
+ next: NextPluginCallback,
55
+ driver: ExternalDriver,
56
+ strategy: string,
57
+ selector: string
58
+ ): Promise<Element[]> {
59
+ return (await this._find(true, next, driver, strategy, selector)) as Element[];
60
+ }
61
+
62
+ private async _find(
63
+ multiple: false,
64
+ next: NextPluginCallback,
65
+ driver: ExternalDriver,
66
+ strategy: string,
67
+ selector: string
68
+ ): Promise<Element>;
69
+ private async _find(
70
+ multiple: true,
71
+ next: NextPluginCallback,
72
+ driver: ExternalDriver,
73
+ strategy: string,
74
+ selector: string
75
+ ): Promise<Element[]>;
76
+ private async _find(
77
+ multiple: boolean,
78
+ next: NextPluginCallback,
79
+ driver: ExternalDriver,
80
+ strategy: string,
81
+ selector: string
82
+ ): Promise<Element | Element[]> {
83
+ const platformName = getPlatformName(driver);
84
+ if (strategy.toLowerCase() !== 'xpath' || !driver.getCurrentContext || (await driver.getCurrentContext()) !== 'NATIVE_APP') {
85
+ return await next() as Element | Element[];
86
+ }
87
+ const xml = await this.getPageSource(null, driver, null, true);
88
+ let newSelector = transformQuery(selector, xml, multiple);
89
+
90
+ // if the selector was not able to be transformed, that means no elements were found that
91
+ // matched, so do the appropriate thing based on element vs elements
92
+ if (newSelector === null) {
93
+ this.log.warn(
94
+ `Selector was not able to be translated to underlying XML. Either the requested ` +
95
+ `element does not exist or there was an error in translation`
96
+ );
97
+ if (multiple) {
98
+ return [];
99
+ }
100
+ throw new errors.NoSuchElementError();
101
+ }
102
+
103
+ if (platformName.toLowerCase() === 'ios') {
104
+ // with the XCUITest driver, the <AppiumAUT> wrapper element is present in the source but is
105
+ // not present in the source considered by WDA, so our index path based xpath queries will
106
+ // not work with WDA as-is. We need to remove the first path segment.
107
+ newSelector = newSelector.replace(/^\/\*\[1\]/, '');
108
+ }
109
+ this.log.info(`Selector was translated to: ${newSelector}`);
110
+
111
+ // otherwise just run the transformed query!
112
+ const finder = multiple ? 'findElements' : 'findElement';
113
+ return await driver[finder](strategy, newSelector) as Element | Element[];
114
+ }
115
+ }
116
+
117
+ function getPlatformName(driver: ExternalDriver): string {
118
+ return ((driver.caps as any)?.platformName as string) || '';
119
+ }
package/lib/source.ts ADDED
@@ -0,0 +1,265 @@
1
+ import _ from 'lodash';
2
+ import {XMLBuilder, XMLParser} from 'fast-xml-parser';
3
+ import NODE_MAP from './node-map';
4
+ import {ATTR_MAP, REMOVE_ATTRS} from './attr-map';
5
+ import * as TRANSFORMS from './transformers';
6
+ import type {
7
+ NodesAndAttributes,
8
+ TransformSourceXmlOptions,
9
+ TransformNodeOptions,
10
+ UniversalNameMap,
11
+ TransformMetadata,
12
+ } from './types';
13
+
14
+ export const ATTR_PREFIX = '@_';
15
+ export const IDX_PATH_PREFIX = `${ATTR_PREFIX}indexPath`;
16
+ export const IDX_PREFIX = `${ATTR_PREFIX}index`;
17
+
18
+ const isAttr = (k: string): boolean => k.startsWith(ATTR_PREFIX);
19
+ const isNode = (k: string): boolean => !isAttr(k);
20
+
21
+ /**
22
+ * Transforms source XML to universal format
23
+ *
24
+ * @param xmlStr - The XML string to transform
25
+ * @param platform - The platform name ('ios' or 'android')
26
+ * @param opts - Transformation options
27
+ * @param opts.metadata - Optional metadata object
28
+ * @param opts.addIndexPath - Whether to add index path attributes
29
+ * @returns Promise resolving to transformed XML and unknown nodes/attributes
30
+ */
31
+ export async function transformSourceXml(
32
+ xmlStr: string,
33
+ platform: string,
34
+ {metadata = {} as TransformMetadata, addIndexPath = false}: TransformSourceXmlOptions = {}
35
+ ): Promise<{xml: string; unknowns: NodesAndAttributes}> {
36
+ // first thing we want to do is modify the ios source root node, because it doesn't include the
37
+ // necessary index attribute, so we add it if it's not there
38
+ xmlStr = xmlStr.replace('<AppiumAUT>', '<AppiumAUT index="0">');
39
+ const xmlObj = singletonXmlParser().parse(xmlStr);
40
+ const unknowns = transformNode(xmlObj, platform, {
41
+ metadata,
42
+ addIndexPath,
43
+ parentPath: '',
44
+ });
45
+ let transformedXml = singletonXmlBuilder().build(xmlObj).trim();
46
+ transformedXml = `<?xml version="1.0" encoding="UTF-8"?>\n${transformedXml}`;
47
+ return {xml: transformedXml, unknowns};
48
+ }
49
+
50
+ /**
51
+ * Gets the universal name for a platform-specific name from a name map
52
+ *
53
+ * @param nameMap - The name mapping object
54
+ * @param name - The platform-specific name
55
+ * @param platform - The platform name
56
+ * @returns The universal name or null if not found
57
+ */
58
+ function getUniversalName(
59
+ nameMap: UniversalNameMap | Readonly<UniversalNameMap>,
60
+ name: string,
61
+ platform: string
62
+ ): string | null {
63
+ for (const translatedName of Object.keys(nameMap)) {
64
+ const sourceNodes = nameMap[translatedName]?.[platform];
65
+ if (_.isArray(sourceNodes) && sourceNodes.includes(name)) {
66
+ return translatedName;
67
+ }
68
+ if (sourceNodes === name) {
69
+ return translatedName;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Gets the universal node name for a platform-specific node name
77
+ *
78
+ * @param nodeName - The platform-specific node name
79
+ * @param platform - The platform name
80
+ * @returns The universal node name or null if not found
81
+ */
82
+ export function getUniversalNodeName(nodeName: string, platform: string): string | null {
83
+ return getUniversalName(NODE_MAP, nodeName, platform);
84
+ }
85
+
86
+ /**
87
+ * Gets the universal attribute name for a platform-specific attribute name
88
+ *
89
+ * @param attrName - The platform-specific attribute name
90
+ * @param platform - The platform name
91
+ * @returns The universal attribute name or null if not found
92
+ */
93
+ export function getUniversalAttrName(attrName: string, platform: string): string | null {
94
+ return getUniversalName(ATTR_MAP, attrName, platform);
95
+ }
96
+
97
+ /**
98
+ * Transforms a node object recursively
99
+ *
100
+ * @param nodeObj - The node object to transform
101
+ * @param platform - The platform name
102
+ * @param opts - Transformation options
103
+ * @returns Object containing unknown nodes and attributes
104
+ */
105
+ export function transformNode(
106
+ nodeObj: any,
107
+ platform: string,
108
+ {metadata, addIndexPath, parentPath}: TransformNodeOptions
109
+ ): NodesAndAttributes {
110
+ const unknownNodes: string[] = [];
111
+ const unknownAttrs: string[] = [];
112
+ if (_.isPlainObject(nodeObj)) {
113
+ const keys = Object.keys(nodeObj);
114
+ const childNodeNames = keys.filter(isNode);
115
+ const attrs = keys.filter(isAttr);
116
+ let thisIndexPath = parentPath || '';
117
+
118
+ if (attrs.length && addIndexPath) {
119
+ if (!attrs.includes(IDX_PREFIX)) {
120
+ throw new Error(`Index path is required but node found with no 'index' attribute`);
121
+ }
122
+
123
+ thisIndexPath = `${parentPath || ''}/${nodeObj[IDX_PREFIX]}`;
124
+ nodeObj[IDX_PATH_PREFIX] = thisIndexPath;
125
+ }
126
+
127
+ const transformFn = TRANSFORMS[platform as keyof typeof TRANSFORMS];
128
+ if (transformFn) {
129
+ transformFn(nodeObj, metadata || ({} as TransformMetadata));
130
+ }
131
+ unknownAttrs.push(...transformAttrs(nodeObj, attrs, platform));
132
+ const unknowns = transformChildNodes(nodeObj, childNodeNames, platform, {
133
+ metadata,
134
+ addIndexPath,
135
+ parentPath: thisIndexPath,
136
+ });
137
+ unknownAttrs.push(...unknowns.attrs);
138
+ unknownNodes.push(...unknowns.nodes);
139
+ } else if (_.isArray(nodeObj)) {
140
+ for (const childObj of nodeObj) {
141
+ const {nodes, attrs} = transformNode(childObj, platform, {
142
+ metadata,
143
+ addIndexPath,
144
+ parentPath: parentPath || '',
145
+ });
146
+ unknownNodes.push(...nodes);
147
+ unknownAttrs.push(...attrs);
148
+ }
149
+ }
150
+ return {
151
+ nodes: _.uniq(unknownNodes),
152
+ attrs: _.uniq(unknownAttrs),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Transforms child nodes of a node object
158
+ *
159
+ * @param nodeObj - The node object containing child nodes
160
+ * @param childNodeNames - Array of child node names
161
+ * @param platform - The platform name
162
+ * @param opts - Transformation options
163
+ * @returns Object containing unknown nodes and attributes
164
+ */
165
+ export function transformChildNodes(
166
+ nodeObj: any,
167
+ childNodeNames: string[],
168
+ platform: string,
169
+ {metadata, addIndexPath, parentPath}: TransformNodeOptions
170
+ ): NodesAndAttributes {
171
+ const unknownNodes: string[] = [];
172
+ const unknownAttrs: string[] = [];
173
+ for (const nodeName of childNodeNames) {
174
+ // before modifying the name of this child node, recurse down and modify the subtree
175
+ const {nodes, attrs} = transformNode(nodeObj[nodeName], platform, {
176
+ metadata,
177
+ addIndexPath,
178
+ parentPath: parentPath || '',
179
+ });
180
+ unknownNodes.push(...nodes);
181
+ unknownAttrs.push(...attrs);
182
+
183
+ // now translate the node name and replace the subtree with this node
184
+ const universalName = getUniversalNodeName(nodeName, platform);
185
+ if (universalName === null) {
186
+ unknownNodes.push(nodeName);
187
+ continue;
188
+ }
189
+
190
+ // since multiple child node names could map to the same new transformed node name, we can't
191
+ // simply assign nodeObj[universalName] = nodeObj[nodeName]; we need to be sensitive to the
192
+ // situation where the end result is an array of children having the same node name
193
+ if (nodeObj[universalName]) {
194
+ // if we already have a node with the universal name, that means we are mapping a second
195
+ // original node name to the same universal node name, so we just push all its children into
196
+ // the list
197
+ if (_.isArray(nodeObj[universalName])) {
198
+ if (_.isArray(nodeObj[nodeName])) {
199
+ nodeObj[universalName].push(...nodeObj[nodeName]);
200
+ } else {
201
+ nodeObj[universalName].push(nodeObj[nodeName]);
202
+ }
203
+ } else {
204
+ nodeObj[universalName] = [nodeObj[universalName]];
205
+ if (_.isArray(nodeObj[nodeName])) {
206
+ nodeObj[universalName].push(...nodeObj[nodeName]);
207
+ } else {
208
+ nodeObj[universalName].push(nodeObj[nodeName]);
209
+ }
210
+ }
211
+ } else {
212
+ nodeObj[universalName] = nodeObj[nodeName];
213
+ }
214
+ delete nodeObj[nodeName];
215
+ }
216
+ return {nodes: unknownNodes, attrs: unknownAttrs};
217
+ }
218
+
219
+ /**
220
+ * Transforms attributes of a node object
221
+ *
222
+ * @param nodeObj - The node object containing attributes
223
+ * @param attrs - Array of attribute keys
224
+ * @param platform - The platform name
225
+ * @returns Array of unknown attribute names
226
+ */
227
+ export function transformAttrs(nodeObj: any, attrs: string[], platform: string): string[] {
228
+ const unknownAttrs: string[] = [];
229
+ for (const attr of attrs) {
230
+ const cleanAttr = attr.substring(2);
231
+ if ((REMOVE_ATTRS as readonly string[]).includes(cleanAttr)) {
232
+ delete nodeObj[attr];
233
+ continue;
234
+ }
235
+ const universalAttr = getUniversalAttrName(cleanAttr, platform);
236
+ if (universalAttr === null) {
237
+ unknownAttrs.push(cleanAttr);
238
+ continue;
239
+ }
240
+ const newAttr = `${ATTR_PREFIX}${universalAttr}`;
241
+ if (newAttr !== attr) {
242
+ nodeObj[newAttr] = nodeObj[attr];
243
+ delete nodeObj[attr];
244
+ }
245
+ }
246
+ return unknownAttrs;
247
+ }
248
+
249
+ const singletonXmlBuilder = _.memoize(function makeXmlBuilder() {
250
+ return new XMLBuilder({
251
+ ignoreAttributes: false,
252
+ attributeNamePrefix: ATTR_PREFIX,
253
+ suppressBooleanAttributes: false,
254
+ format: true,
255
+ });
256
+ });
257
+
258
+ const singletonXmlParser = _.memoize(function makeXmlParser() {
259
+ return new XMLParser({
260
+ ignoreAttributes: false,
261
+ ignoreDeclaration: true,
262
+ attributeNamePrefix: ATTR_PREFIX,
263
+ isArray: (name, jPath, isLeafNode, isAttribute) => !isAttribute,
264
+ });
265
+ });
@@ -1,10 +1,12 @@
1
1
  import {ATTR_PREFIX} from './source';
2
+ import type {TransformMetadata} from './types';
2
3
 
3
- function ios(nodeObj /*, metadata*/) {
4
- return nodeObj;
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ export function ios(_nodeObj: any): void {
6
+ // iOS transformer does nothing
5
7
  }
6
8
 
7
- function android(nodeObj, metadata) {
9
+ export function android(nodeObj: any, metadata: TransformMetadata): void {
8
10
  // strip android:id from front of id
9
11
  const resId = nodeObj[`${ATTR_PREFIX}resource-id`];
10
12
  if (resId && metadata.appPackage) {
@@ -17,16 +19,11 @@ function android(nodeObj, metadata) {
17
19
  .split(/\[|\]|,/)
18
20
  .filter((str) => str !== '');
19
21
  const [x, y, x2, y2] = boundsArray;
20
- const width = x2 - x;
21
- const height = y2 - y;
22
+ const width = parseInt(x2, 10) - parseInt(x, 10);
23
+ const height = parseInt(y2, 10) - parseInt(y, 10);
22
24
  nodeObj[`${ATTR_PREFIX}x`] = x;
23
25
  nodeObj[`${ATTR_PREFIX}y`] = y;
24
- nodeObj[`${ATTR_PREFIX}width`] = width;
25
- nodeObj[`${ATTR_PREFIX}height`] = height;
26
+ nodeObj[`${ATTR_PREFIX}width`] = width.toString();
27
+ nodeObj[`${ATTR_PREFIX}height`] = height.toString();
26
28
  }
27
29
  }
28
-
29
- export default {
30
- ios,
31
- android,
32
- };
package/lib/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ export interface NodesAndAttributes {
2
+ nodes: string[];
3
+ attrs: string[];
4
+ }
5
+
6
+ /**
7
+ * Metadata used for XML transformations
8
+ */
9
+ export interface TransformMetadata {
10
+ appPackage?: string;
11
+ [key: string]: any;
12
+ }
13
+
14
+ export interface TransformSourceXmlOptions {
15
+ metadata?: TransformMetadata;
16
+ addIndexPath?: boolean;
17
+ }
18
+
19
+ export interface TransformNodeOptions extends TransformSourceXmlOptions {
20
+ parentPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Type for platform-specific name mappings
25
+ * Values can be a string or an array of strings (for many-to-one mappings)
26
+ */
27
+ export type PlatformNameMap = Record<string, string | readonly string[]>;
28
+
29
+ /**
30
+ * Type for universal name maps
31
+ * Keys are universal names, values are platform-specific mappings
32
+ */
33
+ export type UniversalNameMap = Record<string, Partial<PlatformNameMap>>;
@@ -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.3",
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,24 +23,24 @@
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",
32
- "!build/tsconfig.tsbuildinfo"
32
+ "!build/tsconfig.tsbuildinfo",
33
+ "!build/test"
33
34
  ],
34
35
  "scripts": {
35
36
  "test": "npm run test:unit",
36
- "test:smoke": "node -e 'require(\"./index.js\")'",
37
- "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\""
38
39
  },
39
40
  "dependencies": {
40
41
  "@xmldom/xmldom": "0.9.8",
41
- "fast-xml-parser": "5.3.2",
42
- "lodash": "4.17.21",
43
- "source-map-support": "0.5.21",
42
+ "fast-xml-parser": "5.4.2",
43
+ "lodash": "4.17.23",
44
44
  "xpath": "0.0.34"
45
45
  },
46
46
  "peerDependencies": {
@@ -57,5 +57,5 @@
57
57
  "publishConfig": {
58
58
  "access": "public"
59
59
  },
60
- "gitHead": "9004554879687ddad51d3afdf8c711b027efbd99"
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"}