appium-uiautomator2-driver 7.5.2 → 7.6.1
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/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/build/lib/commands/find.js +2 -3
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/css/constants.d.ts +5 -0
- package/build/lib/css/constants.d.ts.map +1 -0
- package/build/lib/css/constants.js +8 -0
- package/build/lib/css/constants.js.map +1 -0
- package/build/lib/css/index.d.ts +11 -0
- package/build/lib/css/index.d.ts.map +1 -0
- package/build/lib/css/index.js +56 -0
- package/build/lib/css/index.js.map +1 -0
- package/build/lib/css/schema.d.ts +3 -0
- package/build/lib/css/schema.d.ts.map +1 -0
- package/build/lib/css/schema.js +28 -0
- package/build/lib/css/schema.js.map +1 -0
- package/build/lib/css/ui-automator-emitter.d.ts +16 -0
- package/build/lib/css/ui-automator-emitter.d.ts.map +1 -0
- package/build/lib/css/ui-automator-emitter.js +121 -0
- package/build/lib/css/ui-automator-emitter.js.map +1 -0
- package/build/lib/driver.d.ts +1 -0
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +7 -0
- package/build/lib/driver.js.map +1 -1
- package/build/lib/logger.d.ts +2 -1
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/session-claim-handler.d.ts +33 -0
- package/build/lib/session-claim-handler.d.ts.map +1 -0
- package/build/lib/session-claim-handler.js +207 -0
- package/build/lib/session-claim-handler.js.map +1 -0
- package/lib/commands/find.ts +3 -4
- package/lib/css/constants.ts +5 -0
- package/lib/css/index.ts +59 -0
- package/lib/css/schema.ts +26 -0
- package/lib/css/ui-automator-emitter.ts +148 -0
- package/lib/driver.ts +11 -0
- package/lib/logger.ts +2 -1
- package/lib/session-claim-handler.ts +292 -0
- package/npm-shrinkwrap.json +165 -601
- package/package.json +8 -8
- package/scripts/e2e-spec-batches.sh +58 -0
- package/scripts/run-functional-tests.sh +13 -5
- package/build/lib/css-converter.d.ts +0 -12
- package/build/lib/css-converter.d.ts.map +0 -1
- package/build/lib/css-converter.js +0 -227
- package/build/lib/css-converter.js.map +0 -1
- package/lib/css-converter.ts +0 -279
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionClaimHandler = exports.SessionClaimHandler = void 0;
|
|
4
|
+
exports.setSharedIpcForTesting = setSharedIpcForTesting;
|
|
5
|
+
exports.resetDriverInstanceIpcForTesting = resetDriverInstanceIpcForTesting;
|
|
6
|
+
const support_1 = require("appium/support");
|
|
7
|
+
const asyncbox_1 = require("asyncbox");
|
|
8
|
+
const promises_1 = require("node:timers/promises");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
class SessionClaimHandler {
|
|
11
|
+
getIpc;
|
|
12
|
+
static CLAIMED_TOPIC = 'uiautomator2:sessionUdidClaimed';
|
|
13
|
+
static CONTENDED_TOPIC = 'uiautomator2:sessionUdidContended';
|
|
14
|
+
static RELEASED_TOPIC = 'uiautomator2:sessionUdidReleased';
|
|
15
|
+
static CONTENTION_PROBE_MS = 10;
|
|
16
|
+
static RELEASE_WAIT_MS = 15000;
|
|
17
|
+
subscriptionsBySessionId = new Map();
|
|
18
|
+
constructor(getIpc) {
|
|
19
|
+
this.getIpc = getIpc;
|
|
20
|
+
}
|
|
21
|
+
/** Subscribe the current session to udid claim messages from other sessions. */
|
|
22
|
+
async registerActiveSession(driver) {
|
|
23
|
+
const ipc = await this.getIpc();
|
|
24
|
+
const udid = driver.opts.udid;
|
|
25
|
+
const sessionId = driver.sessionId;
|
|
26
|
+
if (!ipc || !udid || !sessionId) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.unregisterActiveSession(driver);
|
|
30
|
+
const subscription = ipc.subscribe(SessionClaimHandler.CLAIMED_TOPIC, this.getPublisherId(driver));
|
|
31
|
+
subscription.on('message', (message) => {
|
|
32
|
+
void this.dispatchSessionUdidMessage(driver, udid, sessionId, message);
|
|
33
|
+
});
|
|
34
|
+
this.subscriptionsBySessionId.set(sessionId, subscription);
|
|
35
|
+
}
|
|
36
|
+
/** Unsubscribe the current session from udid claim messages. */
|
|
37
|
+
unregisterActiveSession(driver) {
|
|
38
|
+
const sessionId = driver.sessionId;
|
|
39
|
+
if (!sessionId) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.subscriptionsBySessionId.get(sessionId)?.unsubscribe();
|
|
43
|
+
this.subscriptionsBySessionId.delete(sessionId);
|
|
44
|
+
}
|
|
45
|
+
/** Publish this session's udid so any existing session on the same device can terminate. */
|
|
46
|
+
async claimSessionUdid(driver) {
|
|
47
|
+
const ipc = await this.getIpc();
|
|
48
|
+
if (!ipc) {
|
|
49
|
+
driver.log.debug('Driver-instance IPC is unavailable. Skipping publication of the session udid.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const udid = driver.opts.udid;
|
|
53
|
+
const sessionId = driver.sessionId;
|
|
54
|
+
if (!udid || !sessionId) {
|
|
55
|
+
driver.log.debug('The session udid is not known yet. Skipping udid publication.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const contendingSessionIds = new Set();
|
|
59
|
+
const releasedSessionIds = new Set();
|
|
60
|
+
const contendedSubscription = ipc.subscribe(SessionClaimHandler.CONTENDED_TOPIC, this.getPublisherId(driver));
|
|
61
|
+
const releasedSubscription = ipc.subscribe(SessionClaimHandler.RELEASED_TOPIC, this.getPublisherId(driver));
|
|
62
|
+
contendedSubscription.on('message', (message) => {
|
|
63
|
+
if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
|
|
64
|
+
contendingSessionIds.add(message.data.sessionId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
releasedSubscription.on('message', (message) => {
|
|
68
|
+
if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
|
|
69
|
+
releasedSessionIds.add(message.data.sessionId);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await ipc.publish(SessionClaimHandler.CLAIMED_TOPIC, this.getPublisherId(driver), {
|
|
74
|
+
udid,
|
|
75
|
+
sessionId,
|
|
76
|
+
});
|
|
77
|
+
await (0, promises_1.setTimeout)(SessionClaimHandler.CONTENTION_PROBE_MS);
|
|
78
|
+
if (contendingSessionIds.size === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await (0, asyncbox_1.waitForCondition)(() => [...contendingSessionIds].every((id) => releasedSessionIds.has(id)), {
|
|
83
|
+
waitMs: SessionClaimHandler.RELEASE_WAIT_MS,
|
|
84
|
+
intervalMs: 50,
|
|
85
|
+
});
|
|
86
|
+
driver.log.debug(`Received release confirmation from ` +
|
|
87
|
+
`${support_1.util.pluralize('session', contendingSessionIds.size, true)} for udid '${udid}'`);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
const pendingSessionIds = [...contendingSessionIds].filter((id) => !releasedSessionIds.has(id));
|
|
91
|
+
driver.log.warn(`Timed out after ${SessionClaimHandler.RELEASE_WAIT_MS}ms waiting for ` +
|
|
92
|
+
`${support_1.util.pluralize('session', pendingSessionIds.length, true)} ` +
|
|
93
|
+
`[${pendingSessionIds.join(', ')}] to release udid '${udid}'. ` +
|
|
94
|
+
`Proceeding with session startup.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
contendedSubscription.unsubscribe();
|
|
99
|
+
releasedSubscription.unsubscribe();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** @internal Exposed for unit tests. */
|
|
103
|
+
resetForTesting() {
|
|
104
|
+
for (const subscription of this.subscriptionsBySessionId.values()) {
|
|
105
|
+
subscription.unsubscribe();
|
|
106
|
+
}
|
|
107
|
+
this.subscriptionsBySessionId.clear();
|
|
108
|
+
}
|
|
109
|
+
async dispatchSessionUdidMessage(driver, udid, sessionId, message) {
|
|
110
|
+
try {
|
|
111
|
+
await this.handleSessionUdidMessage(driver, udid, message);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
driver.log.warn(`Could not handle udid claim IPC message for session '${sessionId}': ${msg}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async handleSessionUdidMessage(driver, udid, message) {
|
|
119
|
+
if (!this.isMatchingSessionUdidMessage(message, udid, driver.sessionId ?? undefined)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await this.publishSessionUdidContended(driver, udid);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
driver.log.warn(`Could not publish udid contention message for session '${driver.sessionId}': ${msg}`);
|
|
128
|
+
}
|
|
129
|
+
driver.log.warn(`Session '${message.data.sessionId}' is starting on udid '${udid}', which is already in use ` +
|
|
130
|
+
`by another session identified by ${driver.sessionId}. Running multiple parallel sessions on the same ` +
|
|
131
|
+
`device is highly discouraged. Consider enabling the Appium server's '--session-override' flag ` +
|
|
132
|
+
`and make sure to properly quit the previous session before starting a new one. ` +
|
|
133
|
+
`Terminating the obsolete session.`);
|
|
134
|
+
await this.terminateSessionOnRequest(driver, udid);
|
|
135
|
+
}
|
|
136
|
+
async publishSessionUdidContended(driver, udid) {
|
|
137
|
+
const ipc = await this.getIpc();
|
|
138
|
+
const sessionId = driver.sessionId ?? undefined;
|
|
139
|
+
if (!ipc || !udid || !sessionId) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await ipc.publish(SessionClaimHandler.CONTENDED_TOPIC, this.getPublisherId(driver), {
|
|
143
|
+
udid,
|
|
144
|
+
sessionId,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async terminateSessionOnRequest(driver, udid) {
|
|
148
|
+
const sessionId = driver.sessionId ?? undefined;
|
|
149
|
+
const publisherId = sessionId ? this.getPublisherId(driver) : undefined;
|
|
150
|
+
const { log } = driver;
|
|
151
|
+
try {
|
|
152
|
+
await driver.deleteSession();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
156
|
+
log.warn(`Could not terminate session '${sessionId}' on IPC request: ${msg}`);
|
|
157
|
+
}
|
|
158
|
+
await this.publishSessionUdidReleased(log, udid, sessionId, publisherId);
|
|
159
|
+
}
|
|
160
|
+
async publishSessionUdidReleased(log, udid, sessionId, publisherId) {
|
|
161
|
+
try {
|
|
162
|
+
const ipc = await this.getIpc();
|
|
163
|
+
if (!ipc || !udid || !sessionId || !publisherId) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await ipc.publish(SessionClaimHandler.RELEASED_TOPIC, publisherId, {
|
|
167
|
+
udid,
|
|
168
|
+
sessionId,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
log.warn(`Could not publish udid release message for session '${sessionId}': ${msg}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
getPublisherId(driver) {
|
|
177
|
+
return support_1.node.getObjectId(driver);
|
|
178
|
+
}
|
|
179
|
+
isMatchingSessionUdidMessage(message, udid, sessionId) {
|
|
180
|
+
return message.data.udid === udid && message.data.sessionId !== sessionId;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.SessionClaimHandler = SessionClaimHandler;
|
|
184
|
+
const loadSharedIpc = (0, utils_1.memoize)(async function loadSharedIpc() {
|
|
185
|
+
try {
|
|
186
|
+
const { AppiumIpc } = (await import('appium/driver.js'));
|
|
187
|
+
return AppiumIpc ? new AppiumIpc() : undefined;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
exports.sessionClaimHandler = new SessionClaimHandler(loadSharedIpc);
|
|
194
|
+
/**
|
|
195
|
+
* @internal Exposed for unit tests.
|
|
196
|
+
*/
|
|
197
|
+
function setSharedIpcForTesting(ipc) {
|
|
198
|
+
loadSharedIpc.cache.set(undefined, Promise.resolve(ipc));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @internal Exposed for unit tests.
|
|
202
|
+
*/
|
|
203
|
+
function resetDriverInstanceIpcForTesting() {
|
|
204
|
+
exports.sessionClaimHandler.resetForTesting();
|
|
205
|
+
loadSharedIpc.cache.clear();
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=session-claim-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-claim-handler.js","sourceRoot":"","sources":["../../lib/session-claim-handler.ts"],"names":[],"mappings":"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"}
|
package/lib/commands/find.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {cssToNativeLocator} from '../css';
|
|
2
2
|
import type {Element as AppiumElement} from '@appium/types';
|
|
3
3
|
import type {FindElementOpts} from 'appium-android-driver';
|
|
4
4
|
import type {AndroidUiautomator2Driver} from '../driver';
|
|
@@ -42,11 +42,10 @@ export async function doFindElementOrEls(
|
|
|
42
42
|
params.selector = MAGIC_SCROLLABLE_BY;
|
|
43
43
|
}
|
|
44
44
|
if (params.strategy === 'css selector') {
|
|
45
|
-
params.strategy =
|
|
46
|
-
params.selector = new CssConverter(
|
|
45
|
+
({strategy: params.strategy, selector: params.selector} = await cssToNativeLocator(
|
|
47
46
|
params.selector,
|
|
48
47
|
this.opts.appPackage,
|
|
49
|
-
)
|
|
48
|
+
));
|
|
50
49
|
}
|
|
51
50
|
return (await uiautomator2.jwproxy.command(
|
|
52
51
|
`/element${params.multiple ? 's' : ''}`,
|
package/lib/css/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {errors} from 'appium/driver';
|
|
2
|
+
import {memoize} from '../utils';
|
|
3
|
+
import {UI_AUTOMATOR_EMITTER_KEY, UI_AUTOMATOR_STRATEGY} from './constants';
|
|
4
|
+
import {ATTRIBUTE_SCHEMA} from './schema';
|
|
5
|
+
import {UiAutomatorEmitter} from './ui-automator-emitter';
|
|
6
|
+
import type {CssTransformer, NativeLocator, StrategyKey} from '@appium/css-locator-to-native';
|
|
7
|
+
|
|
8
|
+
export {UI_AUTOMATOR_STRATEGY} from './constants';
|
|
9
|
+
|
|
10
|
+
const emitters = {
|
|
11
|
+
[UI_AUTOMATOR_EMITTER_KEY]: new UiAutomatorEmitter(UI_AUTOMATOR_STRATEGY),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getTransformCss = memoize(async function loadTransformCss(): Promise<CssTransformer> {
|
|
15
|
+
const mod = await import('@appium/css-locator-to-native');
|
|
16
|
+
return mod.createCssTransformer({
|
|
17
|
+
schema: ATTRIBUTE_SCHEMA,
|
|
18
|
+
emitters,
|
|
19
|
+
resolveStrategy(): StrategyKey<typeof emitters> {
|
|
20
|
+
return UI_AUTOMATOR_EMITTER_KEY;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts a CSS selector string into a native locator for the resolved strategy.
|
|
27
|
+
*
|
|
28
|
+
* @param css - CSS selector to transform
|
|
29
|
+
* @param appPackage - Optional application package used to qualify resource ids
|
|
30
|
+
* @returns Native locator strategy name and selector string
|
|
31
|
+
*/
|
|
32
|
+
export async function cssToNativeLocator(
|
|
33
|
+
css: string,
|
|
34
|
+
appPackage?: string | null,
|
|
35
|
+
): Promise<NativeLocator> {
|
|
36
|
+
try {
|
|
37
|
+
const transformCss = await getTransformCss();
|
|
38
|
+
return transformCss(css, {appPackage});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw mapCssError(err, css);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mapCssError(err: unknown, css: string): Error {
|
|
45
|
+
if (isPackageError(err, 'InvalidSelectorError')) {
|
|
46
|
+
return new errors.InvalidSelectorError(`Invalid CSS selector '${css}'`, err);
|
|
47
|
+
}
|
|
48
|
+
if (isPackageError(err, 'UnsupportedSelectorError')) {
|
|
49
|
+
return new errors.InvalidSelectorError(`Unsupported CSS selector '${css}'`, err);
|
|
50
|
+
}
|
|
51
|
+
if (err instanceof Error) {
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
54
|
+
return new Error(String(err));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPackageError(err: unknown, name: string): err is Error {
|
|
58
|
+
return err instanceof Error && err.name === name;
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {AttributeSchema} from '@appium/css-locator-to-native';
|
|
2
|
+
|
|
3
|
+
export const ATTRIBUTE_SCHEMA: AttributeSchema = {
|
|
4
|
+
attributes: {
|
|
5
|
+
checkable: {type: 'boolean'},
|
|
6
|
+
checked: {type: 'boolean'},
|
|
7
|
+
clickable: {type: 'boolean'},
|
|
8
|
+
enabled: {type: 'boolean'},
|
|
9
|
+
focusable: {type: 'boolean'},
|
|
10
|
+
focused: {type: 'boolean'},
|
|
11
|
+
'long-clickable': {type: 'boolean'},
|
|
12
|
+
scrollable: {type: 'boolean'},
|
|
13
|
+
selected: {type: 'boolean'},
|
|
14
|
+
index: {type: 'numeric', aliases: ['nth-child']},
|
|
15
|
+
instance: {type: 'numeric'},
|
|
16
|
+
description: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
aliases: ['content-description', 'content-desc', 'desc', 'accessibility-id'],
|
|
19
|
+
},
|
|
20
|
+
'resource-id': {type: 'string', aliases: ['id']},
|
|
21
|
+
text: {type: 'string'},
|
|
22
|
+
'class-name': {type: 'string'},
|
|
23
|
+
'package-name': {type: 'string'},
|
|
24
|
+
},
|
|
25
|
+
booleanFormat: 'true-false',
|
|
26
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {escapeRegExp} from '../utils';
|
|
2
|
+
import type {
|
|
3
|
+
ParsedAttribute,
|
|
4
|
+
ParsedRule,
|
|
5
|
+
ParsedSelector,
|
|
6
|
+
StrategyEmitter,
|
|
7
|
+
} from '@appium/css-locator-to-native';
|
|
8
|
+
|
|
9
|
+
const BOOLEAN_ATTRS = new Set([
|
|
10
|
+
'checkable',
|
|
11
|
+
'checked',
|
|
12
|
+
'clickable',
|
|
13
|
+
'enabled',
|
|
14
|
+
'focusable',
|
|
15
|
+
'focused',
|
|
16
|
+
'long-clickable',
|
|
17
|
+
'scrollable',
|
|
18
|
+
'selected',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const NUMERIC_ATTRS = new Set(['index', 'instance']);
|
|
22
|
+
|
|
23
|
+
const STRING_ATTRS = new Set(['description', 'resource-id', 'text', 'class-name', 'package-name']);
|
|
24
|
+
|
|
25
|
+
const ID_LOCATOR_PATTERN = /^[a-zA-Z_][a-zA-Z0-9._]*:id\/[\S]+$/;
|
|
26
|
+
|
|
27
|
+
export interface UiAutomatorEmitterContext {
|
|
28
|
+
appPackage?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Converts parsed CSS selectors into UiAutomator selector strings. */
|
|
32
|
+
export class UiAutomatorEmitter implements StrategyEmitter<UiAutomatorEmitterContext> {
|
|
33
|
+
readonly strategy: string;
|
|
34
|
+
|
|
35
|
+
constructor(strategy: string) {
|
|
36
|
+
this.strategy = strategy;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emit(parsed: ParsedSelector, ctx?: UiAutomatorEmitterContext): string {
|
|
40
|
+
return this.emitRule(parsed.rule, ctx);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private emitRule(rule: ParsedRule, ctx?: UiAutomatorEmitterContext): string {
|
|
44
|
+
const parts: string[] = ['new UiSelector()'];
|
|
45
|
+
|
|
46
|
+
const tagName = rule.tag;
|
|
47
|
+
const classNames = [...rule.classes];
|
|
48
|
+
|
|
49
|
+
if (tagName && tagName !== '*') {
|
|
50
|
+
if (classNames.length) {
|
|
51
|
+
parts.push(`.className("${[tagName, ...classNames].join('.')}")`);
|
|
52
|
+
} else {
|
|
53
|
+
parts.push(`.classNameMatches("${tagName}")`);
|
|
54
|
+
}
|
|
55
|
+
} else if (classNames.length) {
|
|
56
|
+
parts.push(`.classNameMatches("${classNames.join('\\.')}")`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (rule.id) {
|
|
60
|
+
parts.push(`.resourceId("${this.formatIdLocator(rule.id, ctx?.appPackage)}")`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const attr of rule.attributes) {
|
|
64
|
+
parts.push(this.formatEntity(attr, ctx));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const pseudo of rule.pseudos) {
|
|
68
|
+
const formatted = this.formatEntity(pseudo, ctx);
|
|
69
|
+
if (formatted) {
|
|
70
|
+
parts.push(formatted);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rule.nested) {
|
|
75
|
+
parts.push(`.childSelector(${this.emitRule(rule.nested, ctx)})`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parts.join('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private formatEntity(attr: ParsedAttribute, ctx?: UiAutomatorEmitterContext): string {
|
|
82
|
+
if (BOOLEAN_ATTRS.has(attr.name)) {
|
|
83
|
+
return this.formatBoolean(attr);
|
|
84
|
+
}
|
|
85
|
+
if (NUMERIC_ATTRS.has(attr.name)) {
|
|
86
|
+
return `.${attr.name}(${attr.value})`;
|
|
87
|
+
}
|
|
88
|
+
if (STRING_ATTRS.has(attr.name)) {
|
|
89
|
+
return this.formatStringAttr(attr, ctx);
|
|
90
|
+
}
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private formatBoolean(attr: ParsedAttribute): string {
|
|
95
|
+
const value = attr.value ?? 'true';
|
|
96
|
+
return `.${toSnakeCase(attr.name)}(${value})`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private formatStringAttr(attr: ParsedAttribute, ctx?: UiAutomatorEmitterContext): string {
|
|
100
|
+
const methodName = toSnakeCase(attr.name);
|
|
101
|
+
let value = attr.value ?? '';
|
|
102
|
+
|
|
103
|
+
if (attr.name === 'resource-id') {
|
|
104
|
+
value = this.formatIdLocator(value, ctx?.appPackage);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (value === '') {
|
|
108
|
+
return `.${methodName}Matches("")`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (attr.operator) {
|
|
112
|
+
case '=':
|
|
113
|
+
return `.${methodName}("${value}")`;
|
|
114
|
+
case '*=':
|
|
115
|
+
if (['description', 'text'].includes(attr.name)) {
|
|
116
|
+
return `.${methodName}Contains("${value}")`;
|
|
117
|
+
}
|
|
118
|
+
return `.${methodName}Matches("${escapeRegExp(value)}")`;
|
|
119
|
+
case '^=':
|
|
120
|
+
if (['description', 'text'].includes(attr.name)) {
|
|
121
|
+
return `.${methodName}StartsWith("${value}")`;
|
|
122
|
+
}
|
|
123
|
+
return `.${methodName}Matches("^${escapeRegExp(value)}")`;
|
|
124
|
+
case '$=':
|
|
125
|
+
return `.${methodName}Matches("${escapeRegExp(value)}$")`;
|
|
126
|
+
case '~=':
|
|
127
|
+
return `.${methodName}Matches("${getWordMatcherRegex(value)}")`;
|
|
128
|
+
default:
|
|
129
|
+
return `.${methodName}("${value}")`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private formatIdLocator(locator: string, appPackage?: string | null): string {
|
|
134
|
+
return ID_LOCATOR_PATTERN.test(locator) ? locator : `${appPackage || 'android'}:id/${locator}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toSnakeCase(str: string): string {
|
|
139
|
+
const tokens = str
|
|
140
|
+
.split('-')
|
|
141
|
+
.map((token) => token.charAt(0).toUpperCase() + token.slice(1).toLowerCase());
|
|
142
|
+
const out = tokens.join('');
|
|
143
|
+
return out.charAt(0).toLowerCase() + out.slice(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getWordMatcherRegex(word: string): string {
|
|
147
|
+
return `\\b(\\w*${escapeRegExp(word)}\\w*)\\b`;
|
|
148
|
+
}
|
package/lib/driver.ts
CHANGED
|
@@ -121,6 +121,7 @@ import {
|
|
|
121
121
|
mobileViewPortRect,
|
|
122
122
|
} from './commands/viewport';
|
|
123
123
|
import {executeMethodMap} from './execute-method-map';
|
|
124
|
+
import {sessionClaimHandler} from './session-claim-handler';
|
|
124
125
|
|
|
125
126
|
// NO_PROXY contains the paths that we never want to proxy to UiAutomator2 server.
|
|
126
127
|
// TODO: Add the list of paths that we never want to proxy to UiAutomator2 server.
|
|
@@ -426,6 +427,10 @@ class AndroidUiautomator2Driver
|
|
|
426
427
|
this.opts.udid = udid;
|
|
427
428
|
// @ts-expect-error do not put random stuff on opts
|
|
428
429
|
this.opts.emPort = emPort;
|
|
430
|
+
|
|
431
|
+
await sessionClaimHandler.registerActiveSession(this);
|
|
432
|
+
await sessionClaimHandler.claimSessionUdid(this);
|
|
433
|
+
|
|
429
434
|
// now that we know our java version and device info, we can create our
|
|
430
435
|
// ADB instance
|
|
431
436
|
this.adb = await this.createADB();
|
|
@@ -499,7 +504,13 @@ class AndroidUiautomator2Driver
|
|
|
499
504
|
return {...sessionData, ...uia2Data};
|
|
500
505
|
}
|
|
501
506
|
|
|
507
|
+
async onIpcInit(): Promise<void> {
|
|
508
|
+
await sessionClaimHandler.registerActiveSession(this);
|
|
509
|
+
}
|
|
510
|
+
|
|
502
511
|
override async deleteSession() {
|
|
512
|
+
sessionClaimHandler.unregisterActiveSession(this);
|
|
513
|
+
|
|
503
514
|
this.log.debug('Deleting UiAutomator2 session');
|
|
504
515
|
|
|
505
516
|
const screenRecordingStopTasks = [
|
package/lib/logger.ts
CHANGED