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.
Files changed (48) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +81 -2
  3. package/WebDriverAgentMac/IntegrationTests/AMVideoRecordingTests.m +57 -0
  4. package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMVideoCommands.h +27 -0
  5. package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMVideoCommands.m +120 -0
  6. package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +17 -0
  7. package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBScreenshotCommands.m +8 -8
  8. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSession.h +31 -0
  9. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSessionWrapper.h +39 -0
  10. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMXCTRunnerDaemonSessionWrapper.m +43 -0
  11. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h +59 -0
  12. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m +74 -0
  13. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h +34 -0
  14. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m +32 -0
  15. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h +44 -0
  16. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +97 -0
  17. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBSession.m +14 -0
  18. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +49 -0
  19. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +49 -0
  20. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMVideoRecorder.h +50 -0
  21. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMVideoRecorder.m +112 -0
  22. package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +64 -0
  23. package/build/lib/commands/helpers.d.ts +4 -0
  24. package/build/lib/commands/helpers.d.ts.map +1 -0
  25. package/build/lib/commands/helpers.js +28 -0
  26. package/build/lib/commands/helpers.js.map +1 -0
  27. package/build/lib/commands/native-record-screen.d.ts +75 -0
  28. package/build/lib/commands/native-record-screen.d.ts.map +1 -0
  29. package/build/lib/commands/native-record-screen.js +139 -0
  30. package/build/lib/commands/native-record-screen.js.map +1 -0
  31. package/build/lib/commands/record-screen.d.ts.map +1 -1
  32. package/build/lib/commands/record-screen.js +2 -28
  33. package/build/lib/commands/record-screen.js.map +1 -1
  34. package/build/lib/driver.d.ts +25 -0
  35. package/build/lib/driver.d.ts.map +1 -1
  36. package/build/lib/driver.js +13 -0
  37. package/build/lib/driver.js.map +1 -1
  38. package/build/lib/execute-method-map.d.ts +18 -0
  39. package/build/lib/execute-method-map.d.ts.map +1 -1
  40. package/build/lib/execute-method-map.js +30 -0
  41. package/build/lib/execute-method-map.js.map +1 -1
  42. package/lib/commands/helpers.ts +30 -0
  43. package/lib/commands/native-record-screen.ts +180 -0
  44. package/lib/commands/record-screen.js +2 -30
  45. package/lib/driver.js +18 -0
  46. package/lib/execute-method-map.ts +30 -0
  47. package/npm-shrinkwrap.json +2 -2
  48. 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, net, tempDir } from 'appium/support';
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>;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-mac2-driver",
3
- "version": "2.0.0",
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.0.0",
9
+ "version": "2.1.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/strongbox": "^0.x",
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "mac",
7
7
  "XCTest"
8
8
  ],
9
- "version": "2.0.0",
9
+ "version": "2.1.0",
10
10
  "author": "Appium Contributors",
11
11
  "license": "Apache-2.0",
12
12
  "repository": {