@appium/base-driver 10.6.0 → 10.7.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/build/lib/basedriver/capabilities.d.ts +1 -1
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +15 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +1 -1
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +2 -1
- package/build/lib/basedriver/commands/find.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +4 -4
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +5 -2
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +23 -24
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +11 -5
- package/build/lib/basedriver/extension-core.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +20 -4
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts.map +1 -1
- package/build/lib/basedriver/ipc.js +6 -4
- package/build/lib/basedriver/ipc.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -2
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +0 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +9 -8
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +19 -20
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +4 -1
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +1 -1
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -2
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +14 -7
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +17 -11
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +13 -13
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +35 -18
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +7 -5
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/test-pages/crash.d.ts.map +1 -0
- package/build/lib/test-pages/crash.js.map +1 -0
- package/build/lib/test-pages/env.d.ts +5 -0
- package/build/lib/test-pages/env.d.ts.map +1 -0
- package/build/lib/test-pages/env.js +12 -0
- package/build/lib/test-pages/env.js.map +1 -0
- package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
- package/build/lib/test-pages/handlers.d.ts.map +1 -0
- package/build/lib/{express/static.js → test-pages/handlers.js} +7 -17
- package/build/lib/test-pages/handlers.js.map +1 -0
- package/build/lib/test-pages/index.d.ts +6 -0
- package/build/lib/test-pages/index.d.ts.map +1 -0
- package/build/lib/test-pages/index.js +35 -0
- package/build/lib/test-pages/index.js.map +1 -0
- package/build/lib/test-pages/static-dir.d.ts +8 -0
- package/build/lib/test-pages/static-dir.d.ts.map +1 -0
- package/build/lib/test-pages/static-dir.js +24 -0
- package/build/lib/test-pages/static-dir.js.map +1 -0
- package/build/lib/test-pages/template.d.ts +3 -0
- package/build/lib/test-pages/template.d.ts.map +1 -0
- package/build/lib/test-pages/template.js +19 -0
- package/build/lib/test-pages/template.js.map +1 -0
- package/build/lib/utils.d.ts +0 -2
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +0 -16
- package/build/lib/utils.js.map +1 -1
- package/lib/basedriver/capabilities.ts +72 -66
- package/lib/basedriver/commands/bidi.ts +1 -1
- package/lib/basedriver/commands/event.ts +10 -5
- package/lib/basedriver/commands/execute.ts +12 -9
- package/lib/basedriver/commands/find.ts +20 -12
- package/lib/basedriver/commands/log.ts +2 -2
- package/lib/basedriver/commands/timeout.ts +17 -8
- package/lib/basedriver/core.ts +14 -14
- package/lib/basedriver/device-settings.ts +4 -8
- package/lib/basedriver/driver.ts +50 -40
- package/lib/basedriver/extension-core.ts +33 -17
- package/lib/basedriver/helpers.ts +57 -26
- package/lib/basedriver/ipc.ts +37 -18
- package/lib/basedriver/validation.ts +13 -6
- package/lib/express/express-logging.ts +14 -17
- package/lib/express/idempotency.ts +6 -6
- package/lib/express/middleware.ts +10 -12
- package/lib/express/server.ts +53 -61
- package/lib/express/websocket.ts +5 -7
- package/lib/helpers/capabilities.ts +5 -4
- package/lib/helpers/extension-command-name.ts +1 -1
- package/lib/helpers/levenshtein-match.ts +20 -11
- package/lib/index.js +2 -1
- package/lib/jsonwp-proxy/protocol-converter.ts +51 -27
- package/lib/jsonwp-proxy/proxy.ts +42 -42
- package/lib/protocol/errors.ts +47 -67
- package/lib/protocol/protocol.ts +116 -72
- package/lib/protocol/routes.ts +9 -9
- package/lib/test-pages/env.ts +9 -0
- package/lib/{express/static.ts → test-pages/handlers.ts} +7 -27
- package/lib/test-pages/index.ts +34 -0
- package/lib/test-pages/static-dir.ts +19 -0
- package/lib/test-pages/template.ts +17 -0
- package/lib/utils.ts +3 -23
- package/package.json +9 -10
- package/tsconfig.json +1 -0
- package/build/lib/express/crash.d.ts.map +0 -1
- package/build/lib/express/crash.js.map +0 -1
- package/build/lib/express/static.d.ts.map +0 -1
- package/build/lib/express/static.js.map +0 -1
- /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
- /package/build/lib/{express → test-pages}/crash.js +0 -0
- /package/lib/{express → test-pages}/crash.ts +0 -0
- /package/{static → test-fixtures/static}/appium.png +0 -0
- /package/{static → test-fixtures/static}/favicon.ico +0 -0
- /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
- /package/{static → test-fixtures/static}/test/frameset.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
- /package/{static → test-fixtures/static}/test/iframes.html +0 -0
- /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
- /package/{static → test-fixtures/static}/test/touch.html +0 -0
- /package/{static → test-fixtures/static}/test/welcome.html +0 -0
package/lib/basedriver/core.ts
CHANGED
|
@@ -14,10 +14,7 @@ import type {
|
|
|
14
14
|
import AsyncLock from 'async-lock';
|
|
15
15
|
import {util} from '@appium/support';
|
|
16
16
|
import os from 'node:os';
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_BASE_PATH,
|
|
19
|
-
PROTOCOLS,
|
|
20
|
-
} from '../constants';
|
|
17
|
+
import {DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
|
21
18
|
import {errors} from '../protocol';
|
|
22
19
|
import {DeviceSettings} from './device-settings';
|
|
23
20
|
import * as helpers from './helpers';
|
|
@@ -31,7 +28,8 @@ const ALL_DRIVERS_MATCH = '*';
|
|
|
31
28
|
const FEATURE_NAME_SEPARATOR = ':';
|
|
32
29
|
|
|
33
30
|
export class DriverCore<const C extends Constraints, Settings extends StringRecord = StringRecord>
|
|
34
|
-
extends ExtensionCore
|
|
31
|
+
extends ExtensionCore
|
|
32
|
+
implements Core<C, Settings>
|
|
35
33
|
{
|
|
36
34
|
/**
|
|
37
35
|
* Make the basedriver version available so for any driver which inherits from this package, we
|
|
@@ -41,7 +39,7 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
|
|
41
39
|
|
|
42
40
|
sessionId: string | null;
|
|
43
41
|
|
|
44
|
-
sessionCreationTimestampMs
|
|
42
|
+
sessionCreationTimestampMs!: number;
|
|
45
43
|
|
|
46
44
|
opts: DriverOpts<C>;
|
|
47
45
|
|
|
@@ -104,7 +102,6 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
|
|
104
102
|
super();
|
|
105
103
|
this._log = this.log; // TODO: remove references to this._log and use this.log instead
|
|
106
104
|
|
|
107
|
-
|
|
108
105
|
// setup state
|
|
109
106
|
this.opts = opts as DriverOpts<C>;
|
|
110
107
|
|
|
@@ -273,19 +270,23 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
|
|
273
270
|
// but better be safe than sorry
|
|
274
271
|
throw new Error(
|
|
275
272
|
`The full feature name must include both the automation name ` +
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
`'${this.opts.automationName}' or the '${ALL_DRIVERS_MATCH}' ` +
|
|
274
|
+
`wildcard to apply the feature to all installed drivers, and ` +
|
|
275
|
+
`the feature name split by a colon. Got '${fullName}' instead`,
|
|
279
276
|
);
|
|
280
277
|
}
|
|
281
278
|
return [
|
|
282
279
|
fullName.substring(0, separatorPos).toLowerCase(),
|
|
283
|
-
fullName.substring(separatorPos + 1)
|
|
280
|
+
fullName.substring(separatorPos + 1),
|
|
284
281
|
];
|
|
285
282
|
};
|
|
286
283
|
const parseFullNames = (fullNames: string[]) => fullNames.map(parseFullName);
|
|
287
|
-
const matches = (
|
|
288
|
-
[
|
|
284
|
+
const matches = (pair: string[]) => {
|
|
285
|
+
const [automationName, featureName] = pair;
|
|
286
|
+
return (
|
|
287
|
+
[currentAutomationName, ALL_DRIVERS_MATCH].includes(automationName) && featureName === name
|
|
288
|
+
);
|
|
289
|
+
};
|
|
289
290
|
|
|
290
291
|
// if we have explicitly denied this feature, return false immediately
|
|
291
292
|
if (!util.isEmpty(this.denyInsecure) && parseFullNames(this.denyInsecure).some(matches)) {
|
|
@@ -406,5 +407,4 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
|
|
406
407
|
this.noCommandTimer = null;
|
|
407
408
|
}
|
|
408
409
|
}
|
|
409
|
-
|
|
410
410
|
}
|
|
@@ -23,7 +23,7 @@ export class DeviceSettings<T extends StringRecord = StringRecord> implements ID
|
|
|
23
23
|
*/
|
|
24
24
|
constructor(
|
|
25
25
|
defaultSettings: T = {} as T,
|
|
26
|
-
onSettingsUpdate: SettingsUpdateListener<T> = async () => {}
|
|
26
|
+
onSettingsUpdate: SettingsUpdateListener<T> = async () => {},
|
|
27
27
|
) {
|
|
28
28
|
this._settings = {...defaultSettings};
|
|
29
29
|
this._onSettingsUpdate = onSettingsUpdate;
|
|
@@ -38,14 +38,14 @@ export class DeviceSettings<T extends StringRecord = StringRecord> implements ID
|
|
|
38
38
|
if (!util.isPlainObject(newSettings)) {
|
|
39
39
|
throw new errors.InvalidArgumentError(
|
|
40
40
|
`Settings update should be called with valid JSON. Got ` +
|
|
41
|
-
`${JSON.stringify(newSettings)} instead
|
|
41
|
+
`${JSON.stringify(newSettings)} instead`,
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (node.getObjectSize({...this._settings, ...newSettings}) >= MAX_SETTINGS_SIZE) {
|
|
46
46
|
throw new errors.InvalidArgumentError(
|
|
47
47
|
`New settings cannot be applied, because the overall ` +
|
|
48
|
-
`object size exceeds the allowed limit of ${util.toReadableSizeString(MAX_SETTINGS_SIZE)}
|
|
48
|
+
`object size exceeds the allowed limit of ${util.toReadableSizeString(MAX_SETTINGS_SIZE)}`,
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -56,11 +56,7 @@ export class DeviceSettings<T extends StringRecord = StringRecord> implements ID
|
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
await this._onSettingsUpdate(
|
|
60
|
-
prop as keyof T,
|
|
61
|
-
newSettings[prop],
|
|
62
|
-
this._settings[prop]
|
|
63
|
-
);
|
|
59
|
+
await this._onSettingsUpdate(prop as keyof T, newSettings[prop], this._settings[prop]);
|
|
64
60
|
this._settings[prop] = newSettings[prop];
|
|
65
61
|
}
|
|
66
62
|
}
|
package/lib/basedriver/driver.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type AsyncLock from 'async-lock';
|
|
1
2
|
import {util} from '@appium/support';
|
|
2
3
|
import {
|
|
3
4
|
BASE_DESIRED_CAP_CONSTRAINTS,
|
|
@@ -24,8 +25,11 @@ import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
|
|
|
24
25
|
import {processCapabilities, validateCaps} from './capabilities';
|
|
25
26
|
import {DriverCore} from './core';
|
|
26
27
|
import * as helpers from './helpers';
|
|
27
|
-
import {resolveExecuteExtensionName} from '../helpers/extension-command-name';
|
|
28
28
|
import {mergePlainObjects} from '../utils';
|
|
29
|
+
import {resolveExecuteExtensionName} from '../helpers/extension-command-name';
|
|
30
|
+
|
|
31
|
+
type CommandInvoker<C extends Constraints> = BaseDriver<C> &
|
|
32
|
+
Record<string, ((...args: any[]) => any) | undefined>;
|
|
29
33
|
|
|
30
34
|
const EVENT_SESSION_INIT = 'newSessionRequested';
|
|
31
35
|
const EVENT_SESSION_START = 'newSessionStarted';
|
|
@@ -34,20 +38,20 @@ const EVENT_SESSION_QUIT_DONE = 'quitSessionFinished';
|
|
|
34
38
|
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
|
|
35
39
|
|
|
36
40
|
export class BaseDriver<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const C extends Constraints,
|
|
42
|
+
CArgs extends StringRecord = StringRecord,
|
|
43
|
+
Settings extends StringRecord = StringRecord,
|
|
44
|
+
CreateResult = DefaultCreateSessionResult<C>,
|
|
45
|
+
DeleteResult = DefaultDeleteSessionResult,
|
|
46
|
+
SessionData extends StringRecord = StringRecord,
|
|
47
|
+
>
|
|
44
48
|
extends DriverCore<C, Settings>
|
|
45
49
|
implements Driver<C, CArgs, Settings, CreateResult, DeleteResult, SessionData>
|
|
46
50
|
{
|
|
47
51
|
cliArgs: CArgs & ServerArgs;
|
|
48
52
|
caps: DriverCaps<C>;
|
|
49
|
-
originalCaps
|
|
50
|
-
desiredCapConstraints
|
|
53
|
+
originalCaps!: W3CDriverCaps<C>;
|
|
54
|
+
desiredCapConstraints!: C;
|
|
51
55
|
server?: AppiumServer;
|
|
52
56
|
serverHost?: string;
|
|
53
57
|
serverPort?: number;
|
|
@@ -69,7 +73,11 @@ export class BaseDriver<
|
|
|
69
73
|
*/
|
|
70
74
|
protected get _desiredCapConstraints(): Readonly<BaseDriverCapConstraints & C> {
|
|
71
75
|
return Object.freeze(
|
|
72
|
-
mergePlainObjects(
|
|
76
|
+
mergePlainObjects(
|
|
77
|
+
{},
|
|
78
|
+
BASE_DESIRED_CAP_CONSTRAINTS,
|
|
79
|
+
this.desiredCapConstraints,
|
|
80
|
+
) as BaseDriverCapConstraints & C,
|
|
73
81
|
);
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -99,8 +107,10 @@ export class BaseDriver<
|
|
|
99
107
|
throw new errors.NoSuchDriverError('The driver was unexpectedly shut down!');
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
const invoker = this as unknown as CommandInvoker<C>;
|
|
111
|
+
const command = invoker[cmd];
|
|
102
112
|
// If we don't have this command, it must not be implemented
|
|
103
|
-
if (!
|
|
113
|
+
if (!command) {
|
|
104
114
|
await this.startNewCommandTimeout();
|
|
105
115
|
throw new errors.NotYetImplementedError();
|
|
106
116
|
}
|
|
@@ -115,14 +125,14 @@ export class BaseDriver<
|
|
|
115
125
|
};
|
|
116
126
|
try {
|
|
117
127
|
return await Promise.race([
|
|
118
|
-
this
|
|
128
|
+
command.call(this, ...args),
|
|
119
129
|
// This promise is needed to monitor if the session has been
|
|
120
130
|
// shut down unexpectedly while the command was running
|
|
121
131
|
new Promise((resolve, reject) => {
|
|
122
132
|
unexpectedShutdownResolver = resolve;
|
|
123
133
|
unexpectedShutdownRejecter = reject;
|
|
124
134
|
this.eventEmitter.once(ON_UNEXPECTED_SHUTDOWN_EVENT, onUnexpectedShutdown);
|
|
125
|
-
})
|
|
135
|
+
}),
|
|
126
136
|
]);
|
|
127
137
|
} finally {
|
|
128
138
|
if (unexpectedShutdownRejecter && unexpectedShutdownResolver) {
|
|
@@ -139,7 +149,11 @@ export class BaseDriver<
|
|
|
139
149
|
// automatic session deletion in this.onCommandTimeout. Of course we don't
|
|
140
150
|
// want to trigger the timer when the user is shutting down the session
|
|
141
151
|
// intentionally
|
|
142
|
-
if (
|
|
152
|
+
if (
|
|
153
|
+
!wasSessionShutdownUnexpectedly &&
|
|
154
|
+
this.isCommandsQueueEnabled &&
|
|
155
|
+
cmd !== DELETE_SESSION_COMMAND
|
|
156
|
+
) {
|
|
143
157
|
// resetting existing timeout
|
|
144
158
|
await this.startNewCommandTimeout();
|
|
145
159
|
}
|
|
@@ -147,13 +161,15 @@ export class BaseDriver<
|
|
|
147
161
|
};
|
|
148
162
|
|
|
149
163
|
const synchronizationKey = BaseDriver.name;
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
const commandsQueueGuard = this.commandsQueueGuard as AsyncLock & {
|
|
165
|
+
queues?: Record<string, unknown[]>;
|
|
166
|
+
};
|
|
167
|
+
const commandsQueueLen: number = commandsQueueGuard.queues?.[synchronizationKey]?.length ?? 0;
|
|
152
168
|
if (this.isCommandsQueueEnabled && commandsQueueLen > 0) {
|
|
153
169
|
this.log.debug(
|
|
154
170
|
`Scheduling the '${cmd}' command to the ${this.constructor.name} commands queue. ` +
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
`${util.pluralize('queue item', commandsQueueLen, true)} ${commandsQueueLen === 1 ? 'is' : 'are'} ` +
|
|
172
|
+
`already waiting for execution.`,
|
|
157
173
|
);
|
|
158
174
|
}
|
|
159
175
|
|
|
@@ -182,7 +198,7 @@ export class BaseDriver<
|
|
|
182
198
|
if (cmd === 'execute') {
|
|
183
199
|
const firstArg = args?.[0];
|
|
184
200
|
if (typeof firstArg === 'string' && firstArg.trim().length > 0) {
|
|
185
|
-
return resolveExecuteExtensionName.call(this
|
|
201
|
+
return resolveExecuteExtensionName.call(this as BaseDriver<Constraints>, firstArg);
|
|
186
202
|
}
|
|
187
203
|
}
|
|
188
204
|
|
|
@@ -240,15 +256,12 @@ export class BaseDriver<
|
|
|
240
256
|
this.log.debug('Running generic full reset');
|
|
241
257
|
|
|
242
258
|
// preserving state
|
|
243
|
-
const currentConfig = {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
]) {
|
|
250
|
-
currentConfig[property] = this[property];
|
|
251
|
-
}
|
|
259
|
+
const currentConfig = {
|
|
260
|
+
implicitWaitMs: this.implicitWaitMs,
|
|
261
|
+
newCommandTimeoutMs: this.newCommandTimeoutMs,
|
|
262
|
+
sessionId: this.sessionId,
|
|
263
|
+
shutdownUnexpectedly: this.shutdownUnexpectedly,
|
|
264
|
+
};
|
|
252
265
|
|
|
253
266
|
try {
|
|
254
267
|
if (this.sessionId !== null) {
|
|
@@ -258,9 +271,7 @@ export class BaseDriver<
|
|
|
258
271
|
await this.createSession(this.originalCaps);
|
|
259
272
|
} finally {
|
|
260
273
|
// always restore state.
|
|
261
|
-
|
|
262
|
-
this[key] = value;
|
|
263
|
-
}
|
|
274
|
+
Object.assign(this, currentConfig);
|
|
264
275
|
}
|
|
265
276
|
await this.clearNewCommandTimeout();
|
|
266
277
|
}
|
|
@@ -313,7 +324,8 @@ export class BaseDriver<
|
|
|
313
324
|
) as DriverCaps<C>;
|
|
314
325
|
caps = fixCaps(caps, this._desiredCapConstraints, this.log) as DriverCaps<C>;
|
|
315
326
|
} catch (e) {
|
|
316
|
-
|
|
327
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
328
|
+
throw new errors.SessionNotCreatedError(message);
|
|
317
329
|
}
|
|
318
330
|
|
|
319
331
|
this.validateDesiredCaps(caps);
|
|
@@ -404,11 +416,7 @@ export class BaseDriver<
|
|
|
404
416
|
this.log.warn(`The following provided capabilities were not recognized by this driver:`);
|
|
405
417
|
for (const cap of extraCaps) {
|
|
406
418
|
const suggestion = getLevenshteinSuggestion(cap, knownCaps);
|
|
407
|
-
this.log.warn(
|
|
408
|
-
suggestion
|
|
409
|
-
? ` ${cap} (did you mean '${suggestion}'?)`
|
|
410
|
-
: ` ${cap}`,
|
|
411
|
-
);
|
|
419
|
+
this.log.warn(suggestion ? ` ${cap} (did you mean '${suggestion}'?)` : ` ${cap}`);
|
|
412
420
|
}
|
|
413
421
|
}
|
|
414
422
|
}
|
|
@@ -421,11 +429,13 @@ export class BaseDriver<
|
|
|
421
429
|
try {
|
|
422
430
|
validateCaps(caps, this._desiredCapConstraints);
|
|
423
431
|
} catch (e) {
|
|
432
|
+
const capError = e instanceof Error ? e : new Error(String(e));
|
|
424
433
|
throw this.log.errorWithException(
|
|
425
434
|
new errors.SessionNotCreatedError(
|
|
426
435
|
`Session capabilities were not valid for the ` +
|
|
427
|
-
|
|
428
|
-
|
|
436
|
+
`following reason(s): ${capError.message}`,
|
|
437
|
+
capError,
|
|
438
|
+
),
|
|
429
439
|
);
|
|
430
440
|
}
|
|
431
441
|
|
|
@@ -9,9 +9,7 @@ import type {
|
|
|
9
9
|
IpcData,
|
|
10
10
|
StringRecord,
|
|
11
11
|
} from '@appium/types';
|
|
12
|
-
import {
|
|
13
|
-
MAX_LOG_BODY_LENGTH,
|
|
14
|
-
} from '../constants';
|
|
12
|
+
import {MAX_LOG_BODY_LENGTH} from '../constants';
|
|
15
13
|
import {errors} from '../protocol';
|
|
16
14
|
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
|
|
17
15
|
import {generateDriverLogPrefix} from './helpers';
|
|
@@ -22,10 +20,9 @@ export class ExtensionCore {
|
|
|
22
20
|
_logPrefix?: string;
|
|
23
21
|
// used to handle driver events
|
|
24
22
|
readonly eventEmitter: NodeJS.EventEmitter;
|
|
25
|
-
protected _log
|
|
23
|
+
protected _log!: AppiumLogger;
|
|
26
24
|
private ipc?: IAppiumIpc;
|
|
27
25
|
|
|
28
|
-
|
|
29
26
|
constructor(logPrefix?: string) {
|
|
30
27
|
this._logPrefix = logPrefix;
|
|
31
28
|
this.bidiEventSubs = {};
|
|
@@ -46,7 +43,9 @@ export class ExtensionCore {
|
|
|
46
43
|
updateBidiCommands(cmds: BidiModuleMap): void {
|
|
47
44
|
const overlappingKeys = Object.keys(cmds).filter((key) => key in this.bidiCommands);
|
|
48
45
|
if (overlappingKeys.length) {
|
|
49
|
-
this.log.warn(
|
|
46
|
+
this.log.warn(
|
|
47
|
+
`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`,
|
|
48
|
+
);
|
|
50
49
|
}
|
|
51
50
|
this.bidiCommands = {
|
|
52
51
|
...this.bidiCommands,
|
|
@@ -74,7 +73,7 @@ export class ExtensionCore {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
// if the command module or method isn't part of our spec, reject
|
|
77
|
-
if (!
|
|
76
|
+
if (!this.bidiCommands[moduleName]?.[methodName]) {
|
|
78
77
|
throw new errors.UnknownCommandError();
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -85,13 +84,19 @@ export class ExtensionCore {
|
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
// If the driver doesn't have this command, it must not be implemented
|
|
88
|
-
|
|
87
|
+
const handler = (this as ExtensionCore & Record<string, unknown>)[command];
|
|
88
|
+
if (typeof handler !== 'function') {
|
|
89
89
|
throw new errors.NotYetImplementedError();
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async executeBidiCommand(
|
|
94
|
-
|
|
93
|
+
async executeBidiCommand(
|
|
94
|
+
bidiCmd: string,
|
|
95
|
+
bidiParams: StringRecord,
|
|
96
|
+
next?: () => Promise<any>,
|
|
97
|
+
driver?: ExtensionCore,
|
|
98
|
+
): Promise<BiDiResultData> {
|
|
99
|
+
const handlerType = next && driver ? 'plugin' : 'driver';
|
|
95
100
|
const [moduleName, methodName] = bidiCmd.split('.');
|
|
96
101
|
this.ensureBidiCommandExists(moduleName, methodName);
|
|
97
102
|
const {command, params} = this.bidiCommands[moduleName][methodName];
|
|
@@ -114,17 +119,26 @@ export class ExtensionCore {
|
|
|
114
119
|
args.push(bidiParams[optionalParam]);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
|
-
const logParams = util.truncateString(JSON.stringify(bidiParams), {
|
|
122
|
+
const logParams = util.truncateString(JSON.stringify(bidiParams), {
|
|
123
|
+
length: MAX_LOG_BODY_LENGTH,
|
|
124
|
+
});
|
|
118
125
|
this.log.debug(
|
|
119
126
|
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to ${handlerType} ` +
|
|
120
127
|
`method '${command}'`,
|
|
121
128
|
);
|
|
122
129
|
// call the handler with the signature appropriate to extension type (plugin or driver)
|
|
123
|
-
const
|
|
124
|
-
|
|
130
|
+
const commandHandler = (
|
|
131
|
+
this as unknown as Record<string, (...handlerArgs: any[]) => Promise<unknown>>
|
|
132
|
+
)[command];
|
|
133
|
+
const response =
|
|
134
|
+
next && driver
|
|
135
|
+
? await commandHandler.call(this, next, driver, ...args)
|
|
136
|
+
: await commandHandler.call(this, ...args);
|
|
137
|
+
const finalResponse: BiDiResultData =
|
|
138
|
+
response === undefined ? {} : (response as BiDiResultData);
|
|
125
139
|
this.log.debug(
|
|
126
140
|
`Responding to bidi command '${bidiCmd}' with ` +
|
|
127
|
-
|
|
141
|
+
`${util.truncateString(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`,
|
|
128
142
|
);
|
|
129
143
|
return finalResponse;
|
|
130
144
|
}
|
|
@@ -145,9 +159,11 @@ export class ExtensionCore {
|
|
|
145
159
|
|
|
146
160
|
ipcSubscribe<T extends IpcData>(topic: string): IIpcSubscription<T> {
|
|
147
161
|
if (!this.ipc) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Cannot subscribe to an IPC topic without an IPC object assigned. ` +
|
|
164
|
+
`This is likely a programming error. ipcSubscribe should be called in the ` +
|
|
165
|
+
`onIpcInit handler or after you are certain that createSession has completed successfully.`,
|
|
166
|
+
);
|
|
151
167
|
}
|
|
152
168
|
return this.ipc.subscribe<T>(topic, generateDriverLogPrefix(this));
|
|
153
169
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import nodeFs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import {log as logger} from './logger';
|
|
3
4
|
import {tempDir, fs, util, timing, node} from '@appium/support';
|
|
@@ -13,14 +14,15 @@ import type {
|
|
|
13
14
|
} from '@appium/types';
|
|
14
15
|
import type {AxiosResponseHeaders, RawAxiosRequestHeaders} from 'axios';
|
|
15
16
|
import type {Readable} from 'node:stream';
|
|
17
|
+
import type {PackageJson} from 'type-fest';
|
|
16
18
|
|
|
17
19
|
// for compat with running tests transpiled and in-place
|
|
18
|
-
export const
|
|
20
|
+
export const BASEDRIVER_VER = readBaseDriverVersion();
|
|
19
21
|
|
|
20
22
|
const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
|
|
21
23
|
const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
|
|
22
24
|
const HTTP_STATUS_NOT_MODIFIED = 304;
|
|
23
|
-
const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
25
|
+
const DEFAULT_REQ_HEADERS: RawAxiosRequestHeaders = Object.freeze({
|
|
24
26
|
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
|
|
25
27
|
});
|
|
26
28
|
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
@@ -31,7 +33,7 @@ const APPLICATIONS_CACHE = new LRUCache<string, CachedAppInfoEntry>({
|
|
|
31
33
|
dispose: ({fullPath}, app) => {
|
|
32
34
|
logger.info(
|
|
33
35
|
`The application '${app}' cached at '${fullPath}' has ` +
|
|
34
|
-
`expired after ${CACHED_APPS_MAX_AGE_MS}ms
|
|
36
|
+
`expired after ${CACHED_APPS_MAX_AGE_MS}ms`,
|
|
35
37
|
);
|
|
36
38
|
if (fullPath) {
|
|
37
39
|
void fs.rimraf(fullPath);
|
|
@@ -51,7 +53,7 @@ process.on('exit', () => {
|
|
|
51
53
|
|
|
52
54
|
const appPaths = [...APPLICATIONS_CACHE.values()].map(({fullPath}) => fullPath);
|
|
53
55
|
logger.debug(
|
|
54
|
-
`Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}
|
|
56
|
+
`Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}`,
|
|
55
57
|
);
|
|
56
58
|
for (const appPath of appPaths) {
|
|
57
59
|
if (!appPath) {
|
|
@@ -98,7 +100,7 @@ interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
|
|
|
98
100
|
*/
|
|
99
101
|
export async function configureApp(
|
|
100
102
|
app: string,
|
|
101
|
-
options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions
|
|
103
|
+
options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions,
|
|
102
104
|
): Promise<string> {
|
|
103
105
|
if (typeof app !== 'string') {
|
|
104
106
|
// immediately shortcircuit if not given an app
|
|
@@ -140,7 +142,7 @@ export async function configureApp(
|
|
|
140
142
|
newApp = path.resolve(process.cwd(), newApp);
|
|
141
143
|
logger.warn(
|
|
142
144
|
`The current application path '${app}' is not absolute ` +
|
|
143
|
-
|
|
145
|
+
`and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`,
|
|
144
146
|
);
|
|
145
147
|
app = newApp;
|
|
146
148
|
}
|
|
@@ -194,7 +196,7 @@ export async function configureApp(
|
|
|
194
196
|
}
|
|
195
197
|
logger.info(
|
|
196
198
|
`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
197
|
-
`or its integrity has been damaged. Deleting it from the internal cache
|
|
199
|
+
`or its integrity has been damaged. Deleting it from the internal cache`,
|
|
198
200
|
);
|
|
199
201
|
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
200
202
|
|
|
@@ -215,10 +217,13 @@ export async function configureApp(
|
|
|
215
217
|
});
|
|
216
218
|
} else {
|
|
217
219
|
const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
|
|
218
|
-
newApp = await fetchApp(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
newApp = await fetchApp(
|
|
221
|
+
stream,
|
|
222
|
+
await tempDir.path({
|
|
223
|
+
prefix: fileName,
|
|
224
|
+
suffix: '',
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
222
227
|
}
|
|
223
228
|
} finally {
|
|
224
229
|
if (!stream.closed) {
|
|
@@ -280,7 +285,8 @@ export async function configureApp(
|
|
|
280
285
|
}
|
|
281
286
|
|
|
282
287
|
verifyAppExtension(newApp, supportedAppExtensions);
|
|
283
|
-
return appCacheKey !== toCacheKey(newApp) &&
|
|
288
|
+
return appCacheKey !== toCacheKey(newApp) &&
|
|
289
|
+
(packageHash || Object.values(remoteAppProps).some(Boolean))
|
|
284
290
|
? await storeAppInCache(newApp)
|
|
285
291
|
: newApp;
|
|
286
292
|
});
|
|
@@ -380,7 +386,9 @@ export function generateDriverLogPrefix(obj: object | null, _sessionId?: string
|
|
|
380
386
|
|
|
381
387
|
// #region Private helpers
|
|
382
388
|
|
|
383
|
-
function parseAppLink(
|
|
389
|
+
function parseAppLink(
|
|
390
|
+
appLink: string,
|
|
391
|
+
): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
|
|
384
392
|
try {
|
|
385
393
|
return new URL(appLink);
|
|
386
394
|
} catch {
|
|
@@ -430,7 +438,10 @@ function toCacheKey(app: string): string {
|
|
|
430
438
|
return app;
|
|
431
439
|
}
|
|
432
440
|
|
|
433
|
-
async function queryAppLink(
|
|
441
|
+
async function queryAppLink(
|
|
442
|
+
appLink: string,
|
|
443
|
+
reqHeaders: RawAxiosRequestHeaders,
|
|
444
|
+
): Promise<RemoteAppData> {
|
|
434
445
|
const url = new URL(appLink);
|
|
435
446
|
// Extract credentials, then remove them from the URL for axios
|
|
436
447
|
const {username, password} = url;
|
|
@@ -451,10 +462,9 @@ async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders)
|
|
|
451
462
|
const {data: stream, headers, status} = await axios(requestOpts);
|
|
452
463
|
return {stream, headers, status};
|
|
453
464
|
} catch (err) {
|
|
454
|
-
throw new Error(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
);
|
|
465
|
+
throw new Error(`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`, {
|
|
466
|
+
cause: err,
|
|
467
|
+
});
|
|
458
468
|
}
|
|
459
469
|
}
|
|
460
470
|
|
|
@@ -480,7 +490,7 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
|
|
|
480
490
|
const {size} = await fs.stat(dstPath);
|
|
481
491
|
logger.debug(
|
|
482
492
|
`The application (${util.toReadableSizeString(size)}) ` +
|
|
483
|
-
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s
|
|
493
|
+
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`,
|
|
484
494
|
);
|
|
485
495
|
// it does not make much sense to approximate the speed for short downloads
|
|
486
496
|
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
|
|
@@ -494,13 +504,16 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
|
|
|
494
504
|
function determineFilename(
|
|
495
505
|
headers: AxiosResponseHeaders | RawAxiosRequestHeaders,
|
|
496
506
|
pathname: string,
|
|
497
|
-
supportedAppExtensions: string[]
|
|
507
|
+
supportedAppExtensions: string[],
|
|
498
508
|
): string {
|
|
499
509
|
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
|
500
510
|
replacement: SANITIZE_REPLACEMENT,
|
|
501
511
|
});
|
|
502
512
|
const extname = path.extname(basename);
|
|
503
|
-
if (
|
|
513
|
+
if (
|
|
514
|
+
headers['content-disposition'] &&
|
|
515
|
+
/^attachment/i.test(String(headers['content-disposition']))
|
|
516
|
+
) {
|
|
504
517
|
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
505
518
|
const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
|
|
506
519
|
if (match) {
|
|
@@ -513,10 +526,12 @@ function determineFilename(
|
|
|
513
526
|
? basename.substring(0, basename.length - extname.length)
|
|
514
527
|
: DEFAULT_BASENAME;
|
|
515
528
|
let resultingExt = extname;
|
|
516
|
-
if (
|
|
529
|
+
if (
|
|
530
|
+
!supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(resultingExt.toLowerCase())
|
|
531
|
+
) {
|
|
517
532
|
logger.info(
|
|
518
533
|
`The current file extension '${resultingExt}' is not supported. ` +
|
|
519
|
-
`Defaulting to '${supportedAppExtensions[0]}'
|
|
534
|
+
`Defaulting to '${supportedAppExtensions[0]}'`,
|
|
520
535
|
);
|
|
521
536
|
resultingExt = supportedAppExtensions[0] as string;
|
|
522
537
|
}
|
|
@@ -524,13 +539,15 @@ function determineFilename(
|
|
|
524
539
|
}
|
|
525
540
|
|
|
526
541
|
function verifyAppExtension(app: string, supportedAppExtensions: string[]): string {
|
|
527
|
-
if (
|
|
542
|
+
if (
|
|
543
|
+
supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(path.extname(app).toLowerCase())
|
|
544
|
+
) {
|
|
528
545
|
return app;
|
|
529
546
|
}
|
|
530
547
|
throw new Error(
|
|
531
548
|
`New app path '${app}' did not have ` +
|
|
532
549
|
`${util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
533
|
-
supportedAppExtensions
|
|
550
|
+
supportedAppExtensions,
|
|
534
551
|
);
|
|
535
552
|
}
|
|
536
553
|
|
|
@@ -544,7 +561,7 @@ async function calculateFileIntegrity(filePath: string): Promise<string> {
|
|
|
544
561
|
|
|
545
562
|
async function isAppIntegrityOk(
|
|
546
563
|
currentPath: string,
|
|
547
|
-
expectedIntegrity: {file?: string; folder?: number} = {}
|
|
564
|
+
expectedIntegrity: {file?: string; folder?: number} = {},
|
|
548
565
|
): Promise<boolean> {
|
|
549
566
|
if (!(await fs.exists(currentPath))) {
|
|
550
567
|
return false;
|
|
@@ -570,6 +587,20 @@ function toNaturalNumber(defaultValue: number, envVarName?: string): number {
|
|
|
570
587
|
return num > 0 ? num : defaultValue;
|
|
571
588
|
}
|
|
572
589
|
|
|
590
|
+
function readBaseDriverVersion(): string {
|
|
591
|
+
const pkgRoot = node.getModuleRootSync('@appium/base-driver', __filename);
|
|
592
|
+
if (!pkgRoot) {
|
|
593
|
+
throw new Error('Cannot find the @appium/base-driver package root');
|
|
594
|
+
}
|
|
595
|
+
const pkg = JSON.parse(
|
|
596
|
+
nodeFs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'),
|
|
597
|
+
) as PackageJson;
|
|
598
|
+
if (typeof pkg.version !== 'string') {
|
|
599
|
+
throw new Error('Invalid `package.json` for @appium/base-driver');
|
|
600
|
+
}
|
|
601
|
+
return pkg.version;
|
|
602
|
+
}
|
|
603
|
+
|
|
573
604
|
export default {
|
|
574
605
|
configureApp,
|
|
575
606
|
isPackageOrBundle,
|