@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.
- package/LICENSE +201 -0
- package/build/lib/attr-map.d.ts +3 -57
- package/build/lib/attr-map.d.ts.map +1 -1
- package/build/lib/attr-map.js +2 -5
- package/build/lib/attr-map.js.map +1 -1
- package/build/lib/index.d.ts +4 -3
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +20 -5
- package/build/lib/index.js.map +1 -1
- package/build/lib/node-map.d.ts +3 -199
- package/build/lib/node-map.d.ts.map +1 -1
- package/build/lib/node-map.js +2 -9
- package/build/lib/node-map.js.map +1 -1
- package/build/lib/plugin.d.ts +7 -6
- package/build/lib/plugin.d.ts.map +1 -1
- package/build/lib/plugin.js +16 -18
- package/build/lib/plugin.js.map +1 -1
- package/build/lib/source.d.ts +41 -47
- package/build/lib/source.d.ts.map +1 -1
- package/build/lib/source.js +104 -47
- package/build/lib/source.js.map +1 -1
- package/build/lib/transformers.d.ts +3 -7
- package/build/lib/transformers.d.ts.map +1 -1
- package/build/lib/transformers.js +9 -10
- package/build/lib/transformers.js.map +1 -1
- package/build/lib/types.d.ts +29 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +3 -0
- package/build/lib/types.js.map +1 -0
- package/build/lib/xpath.d.ts +16 -7
- package/build/lib/xpath.d.ts.map +1 -1
- package/build/lib/xpath.js +19 -16
- package/build/lib/xpath.js.map +1 -1
- package/lib/{attr-map.js → attr-map.ts} +5 -5
- package/lib/{index.js → index.ts} +22 -4
- package/lib/{node-map.js → node-map.ts} +5 -1
- package/lib/plugin.ts +119 -0
- package/lib/source.ts +265 -0
- package/lib/{transformers.js → transformers.ts} +9 -12
- package/lib/types.ts +33 -0
- package/lib/{xpath.js → xpath.ts} +24 -20
- package/package.json +7 -7
- package/tsconfig.json +3 -2
- package/build/lib/logger.d.ts +0 -3
- package/build/lib/logger.d.ts.map +0 -1
- package/build/lib/logger.js +0 -6
- package/build/lib/logger.js.map +0 -1
- package/index.js +0 -7
- package/lib/logger.js +0 -3
- package/lib/plugin.js +0 -81
- package/lib/source.js +0 -226
package/build/lib/xpath.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
export function runQuery(query:
|
|
1
|
+
export declare function runQuery(query: string, xmlStr: string): any[];
|
|
2
2
|
/**
|
|
3
|
+
* Transforms an XPath query to work with the original platform-specific XML
|
|
3
4
|
*
|
|
4
|
-
* @param
|
|
5
|
-
* @param
|
|
6
|
-
* @param
|
|
7
|
-
* @returns
|
|
5
|
+
* @param query - The XPath query to transform
|
|
6
|
+
* @param xmlStr - The transformed XML string
|
|
7
|
+
* @param multiple - Whether to return multiple matches
|
|
8
|
+
* @returns The transformed query string or null if no matches found
|
|
8
9
|
*/
|
|
9
|
-
export function transformQuery(query: string, xmlStr: string, multiple: boolean): string | null;
|
|
10
|
-
|
|
10
|
+
export declare function transformQuery(query: string, xmlStr: string, multiple: boolean): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Gets the value of a node attribute
|
|
13
|
+
*
|
|
14
|
+
* @param node - The XML node
|
|
15
|
+
* @param attr - The attribute name
|
|
16
|
+
* @returns The attribute value
|
|
17
|
+
* @throws {Error} If the attribute doesn't exist
|
|
18
|
+
*/
|
|
19
|
+
export declare function getNodeAttrVal(node: any, attr: string): string;
|
|
11
20
|
//# sourceMappingURL=xpath.d.ts.map
|
package/build/lib/xpath.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xpath.d.ts","sourceRoot":"","sources":["../../lib/xpath.
|
|
1
|
+
{"version":3,"file":"xpath.d.ts","sourceRoot":"","sources":["../../lib/xpath.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE,CAM7D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA2B9F;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM9D"}
|
package/build/lib/xpath.js
CHANGED
|
@@ -17,11 +17,12 @@ function runQuery(query, xmlStr) {
|
|
|
17
17
|
return nodes;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
+
* Transforms an XPath query to work with the original platform-specific XML
|
|
20
21
|
*
|
|
21
|
-
* @param
|
|
22
|
-
* @param
|
|
23
|
-
* @param
|
|
24
|
-
* @returns
|
|
22
|
+
* @param query - The XPath query to transform
|
|
23
|
+
* @param xmlStr - The transformed XML string
|
|
24
|
+
* @param multiple - Whether to return multiple matches
|
|
25
|
+
* @returns The transformed query string or null if no matches found
|
|
25
26
|
*/
|
|
26
27
|
function transformQuery(query, xmlStr, multiple) {
|
|
27
28
|
const nodes = runQuery(query, xmlStr);
|
|
@@ -31,9 +32,9 @@ function transformQuery(query, xmlStr, multiple) {
|
|
|
31
32
|
const newQueries = nodes.map((node) => {
|
|
32
33
|
const indexPath = getNodeAttrVal(node, 'indexPath');
|
|
33
34
|
// at this point indexPath will look like /0/0/1/1/0/1/0/2
|
|
34
|
-
|
|
35
|
+
const newQuery = indexPath
|
|
35
36
|
.substring(1) // remove leading / so we can split
|
|
36
|
-
.split('/') // split into
|
|
37
|
+
.split('/') // split into indexes
|
|
37
38
|
.map((indexStr) => {
|
|
38
39
|
// map to xpath node indexes (1-based)
|
|
39
40
|
const xpathIndex = parseInt(indexStr, 10) + 1;
|
|
@@ -43,19 +44,21 @@ function transformQuery(query, xmlStr, multiple) {
|
|
|
43
44
|
// now to make this a valid xpath from the root, prepend the / we removed earlier
|
|
44
45
|
return `/${newQuery}`;
|
|
45
46
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (multiple) {
|
|
49
|
-
newSelector = newQueries.join(' | ');
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
newSelector = newQueries[0];
|
|
53
|
-
}
|
|
47
|
+
if (newQueries.length === 0) {
|
|
48
|
+
return null;
|
|
54
49
|
}
|
|
55
|
-
return
|
|
50
|
+
return multiple ? newQueries.join(' | ') : newQueries[0];
|
|
56
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Gets the value of a node attribute
|
|
54
|
+
*
|
|
55
|
+
* @param node - The XML node
|
|
56
|
+
* @param attr - The attribute name
|
|
57
|
+
* @returns The attribute value
|
|
58
|
+
* @throws {Error} If the attribute doesn't exist
|
|
59
|
+
*/
|
|
57
60
|
function getNodeAttrVal(node, attr) {
|
|
58
|
-
const attrObjs = Object.values(node.attributes).filter((obj) => obj.name === attr);
|
|
61
|
+
const attrObjs = Object.values(node.attributes || {}).filter((obj) => obj.name === attr);
|
|
59
62
|
if (!attrObjs.length) {
|
|
60
63
|
throw new Error(`Tried to retrieve a node attribute '${attr}' but the node didn't have it`);
|
|
61
64
|
}
|
package/build/lib/xpath.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xpath.js","sourceRoot":"","sources":["../../lib/xpath.
|
|
1
|
+
{"version":3,"file":"xpath.js","sourceRoot":"","sources":["../../lib/xpath.ts"],"names":[],"mappings":";;;;;AAIA,4BAMC;AAUD,wCA2BC;AAUD,wCAMC;AA/DD,iCAA2C;AAC3C,2CAAoD;AACpD,oDAAuB;AAEvB,SAAgB,QAAQ,CAAC,KAAa,EAAE,MAAc;IACpD,MAAM,GAAG,GAAG,IAAI,kBAAS,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,kBAAS,CAAC,QAAQ,CAAC,CAAC;IACxE,2DAA2D;IAC3D,8CAA8C;IAC9C,MAAM,KAAK,GAAG,IAAA,cAAU,EAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACrC,OAAO,KAAc,CAAC;AACxB,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,KAAa,EAAE,MAAc,EAAE,QAAiB;IAC7E,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,gBAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACpC,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACpD,0DAA0D;QAC1D,MAAM,QAAQ,GAAG,SAAS;aACvB,SAAS,CAAC,CAAC,CAAC,CAAC,mCAAmC;aAChD,KAAK,CAAC,GAAG,CAAC,CAAC,qBAAqB;aAChC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YAChB,sCAAsC;YACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;YAC9C,OAAO,KAAK,UAAU,GAAG,CAAC;QAC5B,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY;QAE1B,iFAAiF;QACjF,OAAO,IAAI,QAAQ,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,IAAS,EAAE,IAAY;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC9F,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,+BAA+B,CAAC,CAAC;IAC9F,CAAC;IACD,OAAQ,QAAQ,CAAC,CAAC,CAAS,CAAC,KAAK,CAAC;AACpC,CAAC"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// uses the same format as NODE_MAP in node-map.js
|
|
2
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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>>;
|