appium-mac2-driver 2.0.0 → 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/CHANGELOG.md +7 -0
- package/README.md +81 -2
- package/WebDriverAgentMac/IntegrationTests/AMVideoRecordingTests.m +57 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMVideoCommands.h +27 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMVideoCommands.m +120 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +17 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBScreenshotCommands.m +8 -8
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSession.h +31 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSessionWrapper.h +39 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSessionWrapper.m +43 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h +59 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m +74 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h +34 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m +32 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h +44 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +97 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBSession.m +14 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +49 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +49 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMVideoRecorder.h +50 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMVideoRecorder.m +112 -0
- package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +64 -0
- package/build/lib/commands/helpers.d.ts +4 -0
- package/build/lib/commands/helpers.d.ts.map +1 -0
- package/build/lib/commands/helpers.js +28 -0
- package/build/lib/commands/helpers.js.map +1 -0
- package/build/lib/commands/native-record-screen.d.ts +75 -0
- package/build/lib/commands/native-record-screen.d.ts.map +1 -0
- package/build/lib/commands/native-record-screen.js +139 -0
- package/build/lib/commands/native-record-screen.js.map +1 -0
- package/build/lib/commands/record-screen.d.ts.map +1 -1
- package/build/lib/commands/record-screen.js +2 -28
- package/build/lib/commands/record-screen.js.map +1 -1
- package/build/lib/driver.d.ts +25 -0
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +13 -0
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +18 -0
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +30 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/helpers.ts +30 -0
- package/lib/commands/native-record-screen.ts +180 -0
- package/lib/commands/record-screen.js +2 -30
- package/lib/driver.js +18 -0
- package/lib/execute-method-map.ts +30 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fs, util } from 'appium/support';
|
|
4
|
+
import type { Mac2Driver } from '../driver';
|
|
5
|
+
import { uploadRecordedMedia } from './helpers';
|
|
6
|
+
import type { StringRecord } from '@appium/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initiates a new native screen recording session via XCTest.
|
|
10
|
+
* If the screen recording is already running then this call results in noop.
|
|
11
|
+
* A screen recording is running until a testing session is finished.
|
|
12
|
+
* If a recording has never been stopped explicitly during a test session
|
|
13
|
+
* then it would be stopped automatically upon test session termination,
|
|
14
|
+
* and leftover videos would be deleted as well.
|
|
15
|
+
*
|
|
16
|
+
* @since Xcode 15
|
|
17
|
+
* @param fps Frame Per Second setting for the resulting screen recording. 24 by default.
|
|
18
|
+
* @param codec Possible codec value, where `0` means H264 (the default setting), `1` means HEVC
|
|
19
|
+
* @param displayId Valid display identifier to record the video from. Main display is assumed
|
|
20
|
+
* by default.
|
|
21
|
+
* @returns The information about the asynchronously running video recording.
|
|
22
|
+
*/
|
|
23
|
+
export async function macosStartNativeScreenRecording(
|
|
24
|
+
this: Mac2Driver,
|
|
25
|
+
fps?: number,
|
|
26
|
+
codec?: number,
|
|
27
|
+
displayId?: number,
|
|
28
|
+
): Promise<ActiveVideoInfo> {
|
|
29
|
+
const result = await this.wda.proxy.command('/wda/video/start', 'POST', {
|
|
30
|
+
fps,
|
|
31
|
+
codec,
|
|
32
|
+
displayId,
|
|
33
|
+
}) as ActiveVideoInfo;
|
|
34
|
+
this._recordedVideoIds.add(result.uuid);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @since Xcode 15
|
|
40
|
+
* @returns The information about the asynchronously running video recording or
|
|
41
|
+
* null if no native video recording has been started.
|
|
42
|
+
*/
|
|
43
|
+
export async function macosGetNativeScreenRecordingInfo(
|
|
44
|
+
this: Mac2Driver
|
|
45
|
+
): Promise<ActiveVideoInfo | null> {
|
|
46
|
+
return await this.wda.proxy.command('/wda/video', 'GET') as ActiveVideoInfo | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stops native screen recordind.
|
|
51
|
+
* If no screen recording has been started before then the method throws an exception.
|
|
52
|
+
*
|
|
53
|
+
* @since Xcode 15
|
|
54
|
+
* @param remotePath The path to the remote location, where the resulting video should be uploaded.
|
|
55
|
+
* The following protocols are supported: http/https, ftp.
|
|
56
|
+
* Null or empty string value (the default setting) means the content of resulting
|
|
57
|
+
* file should be encoded as Base64 and passed as the endpoint response value.
|
|
58
|
+
* An exception will be thrown if the generated media file is too big to
|
|
59
|
+
* fit into the available process memory.
|
|
60
|
+
* @param user The name of the user for the remote authentication.
|
|
61
|
+
* @param pass The password for the remote authentication.
|
|
62
|
+
* @param method The http multipart upload method name. The 'PUT' one is used by default.
|
|
63
|
+
* @param headers Additional headers mapping for multipart http(s) uploads
|
|
64
|
+
* @param fileFieldName The name of the form field, where the file content BLOB should
|
|
65
|
+
* be stored for http(s) uploads
|
|
66
|
+
* @param formFields Additional form fields for multipart http(s) uploads
|
|
67
|
+
* @returns Base64-encoded content of the recorded media file if 'remotePath'
|
|
68
|
+
* parameter is falsy or an empty string.
|
|
69
|
+
* @throws {Error} If there was an error while getting the name of a media file
|
|
70
|
+
* or the file content cannot be uploaded to the remote location
|
|
71
|
+
* or screen recording is not supported on the device under test.
|
|
72
|
+
*/
|
|
73
|
+
export async function macosStopNativeScreenRecording(
|
|
74
|
+
this: Mac2Driver,
|
|
75
|
+
remotePath?: string,
|
|
76
|
+
user?: string,
|
|
77
|
+
pass?: string,
|
|
78
|
+
method?: string,
|
|
79
|
+
headers?: StringRecord|[string, any][],
|
|
80
|
+
fileFieldName?: string,
|
|
81
|
+
formFields?: StringRecord|[string, string][],
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
const response: ActiveVideoInfo | null = (
|
|
84
|
+
await this.wda.proxy.command('/wda/video/stop', 'POST', {})
|
|
85
|
+
) as ActiveVideoInfo | null;
|
|
86
|
+
if (!response || !_.isPlainObject(response)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'There is no active screen recording, thus nothing to stop. Did you start it before?'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { uuid } = response;
|
|
93
|
+
const matchedVideoPath = _.first(
|
|
94
|
+
(await listAttachments()).filter((name) => name.endsWith(uuid))
|
|
95
|
+
);
|
|
96
|
+
if (!matchedVideoPath) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`The screen recording identified by ${uuid} has not been found. Is it accessible?`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const options = {
|
|
102
|
+
user,
|
|
103
|
+
pass,
|
|
104
|
+
method,
|
|
105
|
+
headers,
|
|
106
|
+
fileFieldName,
|
|
107
|
+
formFields
|
|
108
|
+
};
|
|
109
|
+
const result = await uploadRecordedMedia.bind(this)(matchedVideoPath, remotePath, options);
|
|
110
|
+
await cleanupNativeRecordedVideos.bind(this)(uuid);
|
|
111
|
+
this._recordedVideoIds.delete(uuid);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Deletes previously recorded videos with given ids.
|
|
117
|
+
* This call is safe and does not raise any errors.
|
|
118
|
+
*
|
|
119
|
+
* @param uuids One or more video UUIDs to be deleted
|
|
120
|
+
*/
|
|
121
|
+
export async function cleanupNativeRecordedVideos(
|
|
122
|
+
this: Mac2Driver,
|
|
123
|
+
uuids: string | Set<string>,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const attachments = await listAttachments();
|
|
126
|
+
if (_.isEmpty(attachments)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const tasks: Promise<any>[] = attachments
|
|
130
|
+
.map((attachmentPath) => [path.basename(attachmentPath), attachmentPath])
|
|
131
|
+
.filter(([name,]) => _.isString(uuids) ? uuids === name : uuids.has(name))
|
|
132
|
+
.map(([, attachmentPath]) => fs.rimraf(attachmentPath));
|
|
133
|
+
if (_.isEmpty(tasks)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await Promise.all(tasks);
|
|
138
|
+
this.log.debug(
|
|
139
|
+
`Successfully deleted ${util.pluralize('leftover video recording', tasks.length, true)}`
|
|
140
|
+
);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
this.log.warn(`Could not cleanup some leftover video recordings: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetches information about available displays
|
|
148
|
+
*
|
|
149
|
+
* @returns A map where keys are display identifiers and values are display infos
|
|
150
|
+
*/
|
|
151
|
+
export async function macosListDisplays(this: Mac2Driver): Promise<StringRecord<DisplayInfo>> {
|
|
152
|
+
return await this.wda.proxy.command('/wda/displays/list', 'GET') as StringRecord<DisplayInfo>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// #region Private functions
|
|
156
|
+
|
|
157
|
+
async function listAttachments(): Promise<string[]> {
|
|
158
|
+
// The expected path looks like
|
|
159
|
+
// $HOME/Library/Daemon Containers/EFDD24BF-F856-411F-8954-CD5F0D6E6F3E/Data/Attachments/CAE7E5E2-5AC9-4D33-A47B-C491D644DE06
|
|
160
|
+
const deamonContainersRoot = path.resolve(process.env.HOME as string, 'Library', 'Daemon Containers');
|
|
161
|
+
return await fs.glob(`*/Data/Attachments/*`, {
|
|
162
|
+
cwd: deamonContainersRoot,
|
|
163
|
+
absolute: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface ActiveVideoInfo {
|
|
168
|
+
fps: number;
|
|
169
|
+
codec: number;
|
|
170
|
+
displayId: number;
|
|
171
|
+
uuid: string;
|
|
172
|
+
startedAt: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface DisplayInfo {
|
|
176
|
+
id: number;
|
|
177
|
+
isMain: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// #endregion
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import { waitForCondition } from 'asyncbox';
|
|
3
|
-
import { util, fs,
|
|
3
|
+
import { util, fs, tempDir } from 'appium/support';
|
|
4
4
|
import { SubProcess } from 'teen_process';
|
|
5
5
|
import B from 'bluebird';
|
|
6
|
+
import { uploadRecordedMedia } from './helpers';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
const RETRY_PAUSE = 300;
|
|
@@ -14,35 +15,6 @@ const FFMPEG_BINARY = 'ffmpeg';
|
|
|
14
15
|
const DEFAULT_FPS = 15;
|
|
15
16
|
const DEFAULT_PRESET = 'veryfast';
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
* @this {Mac2Driver}
|
|
20
|
-
* @param {string} localFile
|
|
21
|
-
* @param {string?} remotePath
|
|
22
|
-
* @param {import('@appium/types').StringRecord} [uploadOptions={}]
|
|
23
|
-
* @returns
|
|
24
|
-
*/
|
|
25
|
-
async function uploadRecordedMedia (localFile, remotePath = null, uploadOptions = {}) {
|
|
26
|
-
if (_.isEmpty(remotePath) || _.isNil(remotePath)) {
|
|
27
|
-
const {size} = await fs.stat(localFile);
|
|
28
|
-
this.log.debug(`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`);
|
|
29
|
-
return (await util.toInMemoryBase64(localFile)).toString();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const {user, pass, method, headers, fileFieldName, formFields} = uploadOptions;
|
|
33
|
-
const options = {
|
|
34
|
-
method: method || 'PUT',
|
|
35
|
-
headers,
|
|
36
|
-
fileFieldName,
|
|
37
|
-
formFields,
|
|
38
|
-
};
|
|
39
|
-
if (user && pass) {
|
|
40
|
-
options.auth = {user, pass};
|
|
41
|
-
}
|
|
42
|
-
await net.uploadFile(localFile, remotePath, options);
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
18
|
/**
|
|
47
19
|
* @param {import('@appium/types').AppiumLogger} log
|
|
48
20
|
*/
|
package/lib/driver.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as sourceCommands from './commands/source';
|
|
|
14
14
|
import log from './logger';
|
|
15
15
|
import { newMethodMap } from './method-map';
|
|
16
16
|
import { executeMethodMap } from './execute-method-map';
|
|
17
|
+
import * as nativeScreenRecordingCommands from './commands/native-record-screen';
|
|
17
18
|
|
|
18
19
|
/** @type {import('@appium/types').RouteMatcher[]} */
|
|
19
20
|
const NO_PROXY = [
|
|
@@ -34,6 +35,9 @@ export class Mac2Driver extends BaseDriver {
|
|
|
34
35
|
/** @type {import('./wda-mac').WDAMacServer} */
|
|
35
36
|
wda;
|
|
36
37
|
|
|
38
|
+
/** @type {Set<string>} */
|
|
39
|
+
_recordedVideoIds;
|
|
40
|
+
|
|
37
41
|
static newMethodMap = newMethodMap;
|
|
38
42
|
static executeMethodMap = executeMethodMap;
|
|
39
43
|
|
|
@@ -71,6 +75,7 @@ export class Mac2Driver extends BaseDriver {
|
|
|
71
75
|
this.wda = null;
|
|
72
76
|
this.proxyReqRes = null;
|
|
73
77
|
this.isProxyActive = false;
|
|
78
|
+
this._recordedVideoIds = new Set();
|
|
74
79
|
this._screenRecorder = null;
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -131,6 +136,14 @@ export class Mac2Driver extends BaseDriver {
|
|
|
131
136
|
|
|
132
137
|
async deleteSession () {
|
|
133
138
|
await this._screenRecorder?.stop(true);
|
|
139
|
+
if (!_.isEmpty(this._recordedVideoIds)) {
|
|
140
|
+
try {
|
|
141
|
+
await this.wda.proxy.command('/wda/video/stop', 'POST', {});
|
|
142
|
+
} catch {}
|
|
143
|
+
await nativeScreenRecordingCommands.cleanupNativeRecordedVideos.bind(this)(
|
|
144
|
+
this._recordedVideoIds
|
|
145
|
+
);
|
|
146
|
+
}
|
|
134
147
|
await this.wda.stopSession();
|
|
135
148
|
|
|
136
149
|
if (this.opts.postrun) {
|
|
@@ -187,6 +200,11 @@ export class Mac2Driver extends BaseDriver {
|
|
|
187
200
|
startRecordingScreen = recordScreenCommands.startRecordingScreen;
|
|
188
201
|
stopRecordingScreen = recordScreenCommands.stopRecordingScreen;
|
|
189
202
|
|
|
203
|
+
macosStartNativeScreenRecording = nativeScreenRecordingCommands.macosStartNativeScreenRecording;
|
|
204
|
+
macosGetNativeScreenRecordingInfo = nativeScreenRecordingCommands.macosGetNativeScreenRecordingInfo;
|
|
205
|
+
macosStopNativeScreenRecording = nativeScreenRecordingCommands.macosStopNativeScreenRecording;
|
|
206
|
+
macosListDisplays = nativeScreenRecordingCommands.macosListDisplays;
|
|
207
|
+
|
|
190
208
|
macosScreenshots = screenshotCommands.macosScreenshots;
|
|
191
209
|
|
|
192
210
|
macosSource = sourceCommands.macosSource;
|
|
@@ -302,4 +302,34 @@ export const executeMethodMap = {
|
|
|
302
302
|
],
|
|
303
303
|
},
|
|
304
304
|
},
|
|
305
|
+
'macos: startNativeScreenRecording': {
|
|
306
|
+
command: 'macosStartNativeScreenRecording',
|
|
307
|
+
params: {
|
|
308
|
+
optional: [
|
|
309
|
+
'fps',
|
|
310
|
+
'codec',
|
|
311
|
+
'displayId',
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
'macos: getNativeScreenRecordingInfo': {
|
|
316
|
+
command: 'macosGetNativeScreenRecordingInfo',
|
|
317
|
+
},
|
|
318
|
+
'macos: stopNativeScreenRecording': {
|
|
319
|
+
command: 'macosStopNativeScreenRecording',
|
|
320
|
+
params: {
|
|
321
|
+
optional: [
|
|
322
|
+
'remotePath',
|
|
323
|
+
'user',
|
|
324
|
+
'pass',
|
|
325
|
+
'method',
|
|
326
|
+
'headers',
|
|
327
|
+
'fileFieldName',
|
|
328
|
+
'formFields'
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
'macos: listDisplays': {
|
|
333
|
+
command: 'macosListDisplays',
|
|
334
|
+
},
|
|
305
335
|
} as const satisfies ExecuteMethodMap<any>;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-mac2-driver",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-mac2-driver",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.1.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@appium/strongbox": "^0.x",
|