appium-xcuitest-driver 10.13.4 → 10.14.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/build/lib/commands/condition.d.ts +9 -72
- package/build/lib/commands/condition.d.ts.map +1 -1
- package/build/lib/commands/condition.js +5 -66
- package/build/lib/commands/condition.js.map +1 -1
- package/build/lib/commands/record-audio.d.ts +25 -53
- package/build/lib/commands/record-audio.d.ts.map +1 -1
- package/build/lib/commands/record-audio.js +17 -19
- package/build/lib/commands/record-audio.js.map +1 -1
- package/build/lib/commands/types.d.ts +58 -0
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/commands/xctest-record-screen.d.ts +17 -47
- package/build/lib/commands/xctest-record-screen.d.ts.map +1 -1
- package/build/lib/commands/xctest-record-screen.js +28 -59
- package/build/lib/commands/xctest-record-screen.js.map +1 -1
- package/build/lib/driver.d.ts +1 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +2 -6
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/{condition.js → condition.ts} +21 -77
- package/lib/commands/{record-audio.js → record-audio.ts} +35 -33
- package/lib/commands/types.ts +63 -0
- package/lib/commands/{xctest-record-screen.js → xctest-record-screen.ts} +54 -71
- package/lib/driver.ts +1 -1
- package/lib/execute-method-map.ts +2 -6
- package/npm-shrinkwrap.json +3 -3
- package/package.json +2 -2
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import {INSTRUMENT_CHANNEL, services} from 'appium-ios-device';
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import { isIos18OrNewer } from '../utils';
|
|
4
|
+
import type {XCUITestDriver} from '../driver';
|
|
5
|
+
import type {DVTServiceWithConnection} from 'appium-ios-remotexpc';
|
|
6
|
+
import type {Condition} from './types';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Get all available ConditionInducer configuration information, which can be used with
|
|
7
10
|
* {@linkcode XCUITestDriver.enableConditionInducer}
|
|
8
|
-
* @returns {Promise<Condition[]>}
|
|
9
11
|
* @since 4.9.0
|
|
10
12
|
* @see {@link https://help.apple.com/xcode/mac/current/#/dev308429d42}
|
|
11
|
-
* @this {XCUITestDriver}
|
|
12
13
|
*/
|
|
13
|
-
export async function listConditionInducers() {
|
|
14
|
+
export async function listConditionInducers(this: XCUITestDriver): Promise<Condition[]> {
|
|
14
15
|
requireConditionInducerCompatibleDevice.call(this);
|
|
15
16
|
|
|
16
17
|
if (isIos18OrNewer(this.opts)) {
|
|
17
18
|
const dvtConnection = await startRemoteXPC(this.device.udid);
|
|
18
19
|
try {
|
|
19
20
|
const result = await dvtConnection.conditionInducer.list();
|
|
20
|
-
return
|
|
21
|
-
} catch (err) {
|
|
21
|
+
return result as Condition[];
|
|
22
|
+
} catch (err: any) {
|
|
22
23
|
this.log.error(`Failed to list condition inducers via RemoteXPC: ${err.message}`);
|
|
23
24
|
throw err;
|
|
24
25
|
} finally {
|
|
@@ -50,15 +51,18 @@ export async function listConditionInducers() {
|
|
|
50
51
|
* (Note: the socket needs to remain connected during operation)
|
|
51
52
|
* (Note: Device conditions are available only for real devices running iOS 13.0 and later.)
|
|
52
53
|
*
|
|
53
|
-
* @param
|
|
54
|
-
* @param
|
|
55
|
-
* @returns
|
|
54
|
+
* @param conditionID - Determine which condition IDs are available with the {@linkcode XCUITestDriver.listConditionInducers} command
|
|
55
|
+
* @param profileID - Determine which profile IDs are available with the {@linkcode XCUITestDriver.listConditionInducers} command
|
|
56
|
+
* @returns `true` if enabling the condition succeeded
|
|
56
57
|
* @throws {Error} If you try to start another Condition and the previous Condition has not stopped
|
|
57
58
|
* @since 4.9.0
|
|
58
59
|
* @see {@link https://help.apple.com/xcode/mac/current/#/dev308429d42}
|
|
59
|
-
* @this {XCUITestDriver}
|
|
60
60
|
*/
|
|
61
|
-
export async function enableConditionInducer(
|
|
61
|
+
export async function enableConditionInducer(
|
|
62
|
+
this: XCUITestDriver,
|
|
63
|
+
conditionID: string,
|
|
64
|
+
profileID: string,
|
|
65
|
+
): Promise<boolean> {
|
|
62
66
|
requireConditionInducerCompatibleDevice.call(this);
|
|
63
67
|
|
|
64
68
|
if (isIos18OrNewer(this.opts)) {
|
|
@@ -76,7 +80,7 @@ export async function enableConditionInducer(conditionID, profileID) {
|
|
|
76
80
|
|
|
77
81
|
this.log.info(`Successfully enabled condition profile: ${profileID}`);
|
|
78
82
|
return true;
|
|
79
|
-
} catch (err) {
|
|
83
|
+
} catch (err: any) {
|
|
80
84
|
await closeRemoteXPC.call(this);
|
|
81
85
|
throw this.log.errorWithException(`Condition inducer '${profileID}' cannot be enabled: '${err.message}'`);
|
|
82
86
|
}
|
|
@@ -109,12 +113,11 @@ export async function enableConditionInducer(conditionID, profileID) {
|
|
|
109
113
|
* condition inducer will be automatically disabled
|
|
110
114
|
*
|
|
111
115
|
* (Note: this is also automatically called upon session cleanup)
|
|
112
|
-
* @returns
|
|
116
|
+
* @returns `true` if disable the condition succeeded
|
|
113
117
|
* @since 4.9.0
|
|
114
118
|
* @see {@link https://help.apple.com/xcode/mac/current/#/dev308429d42}
|
|
115
|
-
* @this {XCUITestDriver}
|
|
116
119
|
*/
|
|
117
|
-
export async function disableConditionInducer() {
|
|
120
|
+
export async function disableConditionInducer(this: XCUITestDriver): Promise<boolean> {
|
|
118
121
|
requireConditionInducerCompatibleDevice.call(this);
|
|
119
122
|
|
|
120
123
|
if (isIos18OrNewer(this.opts)) {
|
|
@@ -127,7 +130,7 @@ export async function disableConditionInducer() {
|
|
|
127
130
|
await this._remoteXPCConditionInducerConnection.conditionInducer.disable();
|
|
128
131
|
this.log.info('Successfully disabled condition inducer');
|
|
129
132
|
return true;
|
|
130
|
-
} catch (err) {
|
|
133
|
+
} catch (err: any) {
|
|
131
134
|
this.log.warn(`Failed to disable condition inducer via RemoteXPC: ${err.message}`);
|
|
132
135
|
return false;
|
|
133
136
|
} finally {
|
|
@@ -158,81 +161,22 @@ export async function disableConditionInducer() {
|
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
* @this {XCUITestDriver}
|
|
163
|
-
* @returns {void}
|
|
164
|
-
*/
|
|
165
|
-
function requireConditionInducerCompatibleDevice() {
|
|
164
|
+
function requireConditionInducerCompatibleDevice(this: XCUITestDriver): void {
|
|
166
165
|
if (this.isSimulator()) {
|
|
167
166
|
throw this.log.errorWithException('Condition inducer only works on real devices');
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
* @param {string} udid
|
|
173
|
-
* @returns {Promise<DVTServiceWithConnection>}
|
|
174
|
-
*/
|
|
175
|
-
async function startRemoteXPC(udid) {
|
|
170
|
+
async function startRemoteXPC(udid: string): Promise<DVTServiceWithConnection> {
|
|
176
171
|
const {Services} = await import('appium-ios-remotexpc');
|
|
177
172
|
return Services.startDVTService(udid);
|
|
178
173
|
}
|
|
179
174
|
|
|
180
|
-
|
|
181
|
-
* @this {XCUITestDriver}
|
|
182
|
-
* @returns {Promise<void>}
|
|
183
|
-
*/
|
|
184
|
-
async function closeRemoteXPC() {
|
|
175
|
+
async function closeRemoteXPC(this: XCUITestDriver): Promise<void> {
|
|
185
176
|
if (this._remoteXPCConditionInducerConnection) {
|
|
186
177
|
await this._remoteXPCConditionInducerConnection.remoteXPC.close();
|
|
187
178
|
this._remoteXPCConditionInducerConnection = null;
|
|
188
179
|
}
|
|
189
180
|
}
|
|
190
181
|
|
|
191
|
-
/**
|
|
192
|
-
* @typedef {import('appium-ios-remotexpc', {with: {'resolution-mode': 'import'}}).DVTServiceWithConnection} DVTServiceWithConnection
|
|
193
|
-
*/
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* @typedef {Object} Profile
|
|
197
|
-
* @property {string} name
|
|
198
|
-
* @property {string} identifier the property is profileID used in {@linkcode XCUITestDriver.enableConditionInducer}
|
|
199
|
-
* @property {string} description Configuration details
|
|
200
|
-
*/
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* We can use the returned data to determine whether the Condition is enabled and the currently enabled configuration information
|
|
204
|
-
* @typedef {Object} Condition
|
|
205
|
-
* @property {Profile[]} profiles
|
|
206
|
-
* @property {string} identifier the property is conditionID used in {@linkcode XCUITestDriver.enableConditionInducer}
|
|
207
|
-
* @property {boolean} profilesSorted
|
|
208
|
-
* @property {boolean} isDestructive
|
|
209
|
-
* @property {boolean} isInternal
|
|
210
|
-
* @property {boolean} isActive `true` if this condition identifier is enabled
|
|
211
|
-
* @property {string} activeProfile enabled profiles identifier
|
|
212
|
-
* @example {
|
|
213
|
-
* "profiles": [
|
|
214
|
-
* {
|
|
215
|
-
* "name": "100% packet loss",
|
|
216
|
-
* "identifier": "SlowNetwork100PctLoss", // MobileEnableConditionInducer profileID
|
|
217
|
-
* "description": "Name: 100% Loss Scenario\n
|
|
218
|
-
* Downlink Bandwidth: 0 Mbps\n
|
|
219
|
-
* Downlink Latency:0 ms\n
|
|
220
|
-
* Downlink Packet Loss Ratio: 100%\n
|
|
221
|
-
* Uplink Bandwidth: 0 Mbps\n
|
|
222
|
-
* Uplink Latency: 0 ms\n
|
|
223
|
-
* Uplink Packet Loss Ratio: 100%"
|
|
224
|
-
* }
|
|
225
|
-
* ],
|
|
226
|
-
* "profilesSorted": true,
|
|
227
|
-
* "identifier": "SlowNetworkCondition", // MobileEnableConditionInducer conditionID
|
|
228
|
-
* "isDestructive": false,
|
|
229
|
-
* "isInternal": false,
|
|
230
|
-
* "activeProfile": "",
|
|
231
|
-
* "name": "Network Link",
|
|
232
|
-
* "isActive": false
|
|
233
|
-
* }
|
|
234
|
-
*/
|
|
235
182
|
|
|
236
|
-
/**
|
|
237
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
238
|
-
*/
|
|
@@ -2,6 +2,8 @@ import {fs, tempDir, logger, util} from 'appium/support';
|
|
|
2
2
|
import {SubProcess} from 'teen_process';
|
|
3
3
|
import {encodeBase64OrUpload} from '../utils';
|
|
4
4
|
import {waitForCondition} from 'asyncbox';
|
|
5
|
+
import type {XCUITestDriver} from '../driver';
|
|
6
|
+
import type {AudioRecorderOptions} from './types';
|
|
5
7
|
|
|
6
8
|
const MAX_RECORDING_TIME_SEC = 43200;
|
|
7
9
|
const AUDIO_RECORD_FEAT_NAME = 'audio_record';
|
|
@@ -13,7 +15,13 @@ const FFMPEG_BINARY = 'ffmpeg';
|
|
|
13
15
|
const ffmpegLogger = logger.getLogger(FFMPEG_BINARY);
|
|
14
16
|
|
|
15
17
|
export class AudioRecorder {
|
|
16
|
-
|
|
18
|
+
private readonly input: string | number;
|
|
19
|
+
private readonly log: any;
|
|
20
|
+
private readonly audioPath: string;
|
|
21
|
+
private readonly opts: AudioRecorderOptions;
|
|
22
|
+
private mainProcess: SubProcess | null;
|
|
23
|
+
|
|
24
|
+
constructor(input: string | number, log: any, audioPath: string, opts: AudioRecorderOptions = {} as AudioRecorderOptions) {
|
|
17
25
|
this.input = input;
|
|
18
26
|
this.log = log;
|
|
19
27
|
this.audioPath = audioPath;
|
|
@@ -21,7 +29,7 @@ export class AudioRecorder {
|
|
|
21
29
|
this.mainProcess = null;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
async start(timeoutSeconds) {
|
|
32
|
+
async start(timeoutSeconds: number): Promise<void> {
|
|
25
33
|
try {
|
|
26
34
|
await fs.which(FFMPEG_BINARY);
|
|
27
35
|
} catch {
|
|
@@ -39,7 +47,7 @@ export class AudioRecorder {
|
|
|
39
47
|
'-f',
|
|
40
48
|
audioSource,
|
|
41
49
|
'-i',
|
|
42
|
-
this.input,
|
|
50
|
+
String(this.input),
|
|
43
51
|
'-c:a',
|
|
44
52
|
audioCodec,
|
|
45
53
|
'-b:a',
|
|
@@ -90,7 +98,7 @@ export class AudioRecorder {
|
|
|
90
98
|
);
|
|
91
99
|
this.mainProcess.once('exit', (code, signal) => {
|
|
92
100
|
// ffmpeg returns code 255 if SIGINT arrives
|
|
93
|
-
if ([0, 255].includes(code)) {
|
|
101
|
+
if ([0, 255].includes(code ?? 0)) {
|
|
94
102
|
this.log.info(`The recording session on audio input '${this.input}' has been finished`);
|
|
95
103
|
} else {
|
|
96
104
|
this.log.debug(
|
|
@@ -101,17 +109,17 @@ export class AudioRecorder {
|
|
|
101
109
|
});
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
isRecording() {
|
|
112
|
+
isRecording(): boolean {
|
|
105
113
|
return !!this.mainProcess?.isRunning;
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
async interrupt(force = false) {
|
|
116
|
+
async interrupt(force = false): Promise<boolean> {
|
|
109
117
|
if (this.isRecording()) {
|
|
110
118
|
const interruptPromise = this.mainProcess?.stop(force ? 'SIGTERM' : 'SIGINT');
|
|
111
119
|
this.mainProcess = null;
|
|
112
120
|
try {
|
|
113
121
|
await interruptPromise;
|
|
114
|
-
} catch (e) {
|
|
122
|
+
} catch (e: any) {
|
|
115
123
|
this.log.warn(
|
|
116
124
|
`Cannot ${force ? 'terminate' : 'interrupt'} ${FFMPEG_BINARY}. ` +
|
|
117
125
|
`Original error: ${e.message}`,
|
|
@@ -123,12 +131,12 @@ export class AudioRecorder {
|
|
|
123
131
|
return true;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
async finish() {
|
|
134
|
+
async finish(): Promise<string> {
|
|
127
135
|
await this.interrupt();
|
|
128
136
|
return this.audioPath;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
async cleanup() {
|
|
139
|
+
async cleanup(): Promise<void> {
|
|
132
140
|
if (await fs.exists(this.audioPath)) {
|
|
133
141
|
await fs.rimraf(this.audioPath);
|
|
134
142
|
}
|
|
@@ -140,27 +148,25 @@ export class AudioRecorder {
|
|
|
140
148
|
*
|
|
141
149
|
* **To use this command, the `audio_record` security feature must be enabled _and_ [FFMpeg](https://ffmpeg.org/) must be installed on the Appium server.**
|
|
142
150
|
*
|
|
143
|
-
* @param
|
|
144
|
-
* @param
|
|
145
|
-
* @param
|
|
146
|
-
* @param
|
|
147
|
-
* @param
|
|
148
|
-
* @param
|
|
149
|
-
* @param
|
|
151
|
+
* @param audioInput - The name of the corresponding audio input device to use for the capture. The full list of capture devices could be shown by executing `ffmpeg -f avfoundation -list_devices true -i ""`
|
|
152
|
+
* @param timeLimit - The maximum recording time, in seconds.
|
|
153
|
+
* @param audioCodec - The name of the audio codec.
|
|
154
|
+
* @param audioBitrate - The bitrate of the resulting audio stream.
|
|
155
|
+
* @param audioChannels - The count of audio channels in the resulting stream. Setting it to `1` will create a single channel (mono) audio stream.
|
|
156
|
+
* @param audioRate - The sampling rate of the resulting audio stream (in Hz).
|
|
157
|
+
* @param forceRestart - Whether to restart audio capture process forcefully when `mobile: startRecordingAudio` is called (`true`) or ignore the call until the current audio recording is completed (`false`).
|
|
150
158
|
* @group Real Device Only
|
|
151
|
-
* @this {XCUITestDriver}
|
|
152
|
-
* @returns {Promise<void>}
|
|
153
|
-
* @privateRemarks Using string literals for the default parameters makes better documentation.
|
|
154
159
|
*/
|
|
155
160
|
export async function startAudioRecording(
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
this: XCUITestDriver,
|
|
162
|
+
audioInput: string | number,
|
|
163
|
+
timeLimit: string | number = 180,
|
|
158
164
|
audioCodec = 'aac',
|
|
159
165
|
audioBitrate = '128k',
|
|
160
|
-
audioChannels = 2,
|
|
161
|
-
audioRate = 44100,
|
|
166
|
+
audioChannels: string | number = 2,
|
|
167
|
+
audioRate: string | number = 44100,
|
|
162
168
|
forceRestart = false,
|
|
163
|
-
) {
|
|
169
|
+
): Promise<void> {
|
|
164
170
|
if (!this.isFeatureEnabled(AUDIO_RECORD_FEAT_NAME)) {
|
|
165
171
|
throw this.log.errorWithException(
|
|
166
172
|
`Audio capture feature must be enabled on the server side. ` +
|
|
@@ -203,8 +209,8 @@ export async function startAudioRecording(
|
|
|
203
209
|
audioSource: DEFAULT_SOURCE,
|
|
204
210
|
audioCodec,
|
|
205
211
|
audioBitrate,
|
|
206
|
-
audioChannels,
|
|
207
|
-
audioRate,
|
|
212
|
+
audioChannels: Number(audioChannels),
|
|
213
|
+
audioRate: Number(audioRate),
|
|
208
214
|
});
|
|
209
215
|
|
|
210
216
|
const timeoutSeconds = parseInt(String(timeLimit), 10);
|
|
@@ -231,18 +237,17 @@ export async function startAudioRecording(
|
|
|
231
237
|
* If no previously recorded file is found and no active audio recording
|
|
232
238
|
* processes are running then the method returns an empty string.
|
|
233
239
|
*
|
|
234
|
-
* @returns
|
|
240
|
+
* @returns Base64-encoded content of the recorded media file or an
|
|
235
241
|
* empty string if no audio recording has been started before.
|
|
236
242
|
* @throws {Error} If there was an error while getting the recorded file.
|
|
237
|
-
* @this {XCUITestDriver}
|
|
238
243
|
*/
|
|
239
|
-
export async function stopAudioRecording() {
|
|
244
|
+
export async function stopAudioRecording(this: XCUITestDriver): Promise<string> {
|
|
240
245
|
if (!this._audioRecorder) {
|
|
241
246
|
this.log.info('Audio recording has not been started. There is nothing to stop');
|
|
242
247
|
return '';
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
let resultPath;
|
|
250
|
+
let resultPath: string;
|
|
246
251
|
try {
|
|
247
252
|
resultPath = await this._audioRecorder.finish();
|
|
248
253
|
if (!(await fs.exists(resultPath))) {
|
|
@@ -259,6 +264,3 @@ export async function stopAudioRecording() {
|
|
|
259
264
|
return await encodeBase64OrUpload(resultPath);
|
|
260
265
|
}
|
|
261
266
|
|
|
262
|
-
/**
|
|
263
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
264
|
-
*/
|
package/lib/commands/types.ts
CHANGED
|
@@ -646,3 +646,66 @@ export interface LogEntry {
|
|
|
646
646
|
}
|
|
647
647
|
|
|
648
648
|
export type LogListener = (logEntry: LogEntry) => any;
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Condition inducer profile configuration
|
|
652
|
+
*/
|
|
653
|
+
export interface Profile {
|
|
654
|
+
name: string;
|
|
655
|
+
/** The property is profileID used in {@linkcode XCUITestDriver.enableConditionInducer} */
|
|
656
|
+
identifier: string;
|
|
657
|
+
/** Configuration details */
|
|
658
|
+
description: string;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* We can use the returned data to determine whether the Condition is enabled and the currently enabled configuration information
|
|
663
|
+
*/
|
|
664
|
+
export interface Condition {
|
|
665
|
+
profiles: Profile[];
|
|
666
|
+
/** The property is conditionID used in {@linkcode XCUITestDriver.enableConditionInducer} */
|
|
667
|
+
identifier: string;
|
|
668
|
+
profilesSorted: boolean;
|
|
669
|
+
isDestructive: boolean;
|
|
670
|
+
isInternal: boolean;
|
|
671
|
+
/** `true` if this condition identifier is enabled */
|
|
672
|
+
isActive: boolean;
|
|
673
|
+
/** Enabled profiles identifier */
|
|
674
|
+
activeProfile: string;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Information about an XCTest screen recording session
|
|
679
|
+
*/
|
|
680
|
+
export interface XcTestScreenRecordingInfo {
|
|
681
|
+
/** Unique identifier of the video being recorded */
|
|
682
|
+
uuid: string;
|
|
683
|
+
/** FPS value */
|
|
684
|
+
fps: number;
|
|
685
|
+
/** Video codec, where 0 is h264 */
|
|
686
|
+
codec: number;
|
|
687
|
+
/** The timestamp when the screen recording has started in float Unix seconds */
|
|
688
|
+
startedAt: number;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* XCTest screen recording result with payload
|
|
693
|
+
*/
|
|
694
|
+
export interface XcTestScreenRecording extends XcTestScreenRecordingInfo {
|
|
695
|
+
/**
|
|
696
|
+
* Base64-encoded content of the recorded media file if `remotePath` parameter is empty or null or an empty string otherwise.
|
|
697
|
+
* The media is expected to a be a valid QuickTime movie (.mov).
|
|
698
|
+
*/
|
|
699
|
+
payload: string;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Options for AudioRecorder
|
|
704
|
+
*/
|
|
705
|
+
export interface AudioRecorderOptions {
|
|
706
|
+
audioSource: string;
|
|
707
|
+
audioCodec: string;
|
|
708
|
+
audioBitrate: string;
|
|
709
|
+
audioChannels: number;
|
|
710
|
+
audioRate: number;
|
|
711
|
+
}
|
|
@@ -2,6 +2,11 @@ import _ from 'lodash';
|
|
|
2
2
|
import {fs, util} from 'appium/support';
|
|
3
3
|
import {encodeBase64OrUpload} from '../utils';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import type {XCUITestDriver} from '../driver';
|
|
6
|
+
import type {Simulator} from 'appium-ios-simulator';
|
|
7
|
+
import type {RealDevice} from '../device/real-device-management';
|
|
8
|
+
import type {HTTPHeaders} from '@appium/types';
|
|
9
|
+
import type {XcTestScreenRecordingInfo, XcTestScreenRecording} from './types';
|
|
5
10
|
|
|
6
11
|
const MOV_EXT = '.mov';
|
|
7
12
|
const FEATURE_NAME = 'xctest_screen_record';
|
|
@@ -10,30 +15,9 @@ const DOMAIN_TYPE = 'appDataContainer';
|
|
|
10
15
|
const USERNAME = 'mobile';
|
|
11
16
|
const SUBDIRECTORY = 'Attachments';
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @property {number} fps FPS value
|
|
17
|
-
* @property {number} codec Video codec, where 0 is h264
|
|
18
|
-
* @property {number} startedAt The timestamp when the screen recording has started in float Unix seconds
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {Object} XcTestScreenRecordingType
|
|
23
|
-
* @property {string} payload Base64-encoded content of the recorded media
|
|
24
|
-
* file if `remotePath` parameter is empty or null or an empty string otherwise.
|
|
25
|
-
* The media is expected to a be a valid QuickTime movie (.mov).
|
|
26
|
-
* @typedef {XcTestScreenRecordingInfo & XcTestScreenRecordingType} XcTestScreenRecording
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @this {XCUITestDriver}
|
|
31
|
-
* @param {string} uuid Unique identifier of the video being recorded
|
|
32
|
-
* @returns {Promise<string>} The full path to the screen recording movie
|
|
33
|
-
*/
|
|
34
|
-
async function retrieveRecodingFromSimulator(uuid) {
|
|
35
|
-
const device = /** @type {import('appium-ios-simulator').Simulator} */ (this.device);
|
|
36
|
-
const dataRoot = /** @type {string} */ (device.getDir());
|
|
18
|
+
async function retrieveRecodingFromSimulator(this: XCUITestDriver, uuid: string): Promise<string> {
|
|
19
|
+
const device = this.device as Simulator;
|
|
20
|
+
const dataRoot = device.getDir();
|
|
37
21
|
// On Simulators the path looks like
|
|
38
22
|
// $HOME/Library/Developer/CoreSimulator/Devices/F8E1968A-8443-4A9A-AB86-27C54C36A2F6/data/Containers/Data/InternalDaemon/4E3FE8DF-AD0A-41DA-B6EC-C35E5798C219/Attachments/A044DAF7-4A58-4CD5-95C3-29B4FE80C377
|
|
39
23
|
const internalDaemonRoot = path.resolve(dataRoot, 'Containers', 'Data', 'InternalDaemon');
|
|
@@ -52,13 +36,8 @@ async function retrieveRecodingFromSimulator(uuid) {
|
|
|
52
36
|
return videoPath;
|
|
53
37
|
}
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* @param {string} uuid Unique identifier of the video being recorded
|
|
58
|
-
* @returns {Promise<string>} The full path to the screen recording movie
|
|
59
|
-
*/
|
|
60
|
-
async function retrieveRecodingFromRealDevice(uuid) {
|
|
61
|
-
const device = /** @type {import('../device/real-device-management').RealDevice} */ (this.device);
|
|
39
|
+
async function retrieveRecodingFromRealDevice(this: XCUITestDriver, uuid: string): Promise<string> {
|
|
40
|
+
const device = this.device as RealDevice;
|
|
62
41
|
|
|
63
42
|
const fileNames = await device.devicectl.listFiles(DOMAIN_TYPE, DOMAIN_IDENTIFIER, {
|
|
64
43
|
username: USERNAME,
|
|
@@ -69,7 +48,10 @@ async function retrieveRecodingFromRealDevice(uuid) {
|
|
|
69
48
|
`Unable to locate XCTest screen recording identified by '${uuid}' for the device ${this.opts.udid}`
|
|
70
49
|
);
|
|
71
50
|
}
|
|
72
|
-
|
|
51
|
+
if (!this.opts.tmpDir) {
|
|
52
|
+
throw new Error('tmpDir is not set in driver options');
|
|
53
|
+
}
|
|
54
|
+
const videoPath = path.join(this.opts.tmpDir, `${uuid}${MOV_EXT}`);
|
|
73
55
|
await device.devicectl.pullFile(`${SUBDIRECTORY}/${uuid}`, videoPath, {
|
|
74
56
|
username: USERNAME,
|
|
75
57
|
domainIdentifier: DOMAIN_IDENTIFIER,
|
|
@@ -80,15 +62,10 @@ async function retrieveRecodingFromRealDevice(uuid) {
|
|
|
80
62
|
return videoPath;
|
|
81
63
|
}
|
|
82
64
|
|
|
83
|
-
|
|
84
|
-
* @this {XCUITestDriver}
|
|
85
|
-
* @param {string} uuid Unique identifier of the video being recorded
|
|
86
|
-
* @returns {Promise<string>} The full path to the screen recording movie
|
|
87
|
-
*/
|
|
88
|
-
async function retrieveXcTestScreenRecording(uuid) {
|
|
65
|
+
async function retrieveXcTestScreenRecording(this: XCUITestDriver, uuid: string): Promise<string> {
|
|
89
66
|
return this.isRealDevice()
|
|
90
|
-
? await retrieveRecodingFromRealDevice.
|
|
91
|
-
: await retrieveRecodingFromSimulator.
|
|
67
|
+
? await retrieveRecodingFromRealDevice.call(this, uuid)
|
|
68
|
+
: await retrieveRecodingFromSimulator.call(this, uuid);
|
|
92
69
|
}
|
|
93
70
|
|
|
94
71
|
/**
|
|
@@ -102,14 +79,16 @@ async function retrieveXcTestScreenRecording(uuid) {
|
|
|
102
79
|
* If the recording is already running this API is a noop.
|
|
103
80
|
*
|
|
104
81
|
* @since Xcode 15/iOS 17
|
|
105
|
-
* @param
|
|
106
|
-
* @param
|
|
107
|
-
* @returns
|
|
108
|
-
* about a newly created or a running the screen recording.
|
|
82
|
+
* @param fps - FPS value
|
|
83
|
+
* @param codec - Video codec, where 0 is h264, 1 is HEVC
|
|
84
|
+
* @returns The information about a newly created or a running the screen recording.
|
|
109
85
|
* @throws {Error} If screen recording has failed to start.
|
|
110
|
-
* @this {XCUITestDriver}
|
|
111
86
|
*/
|
|
112
|
-
export async function mobileStartXctestScreenRecording(
|
|
87
|
+
export async function mobileStartXctestScreenRecording(
|
|
88
|
+
this: XCUITestDriver,
|
|
89
|
+
fps?: number,
|
|
90
|
+
codec?: number,
|
|
91
|
+
): Promise<XcTestScreenRecordingInfo> {
|
|
113
92
|
if (this.isRealDevice()) {
|
|
114
93
|
// This feature might be used to abuse real devices as there is no
|
|
115
94
|
// reliable way (yet) to cleanup video recordings stored there
|
|
@@ -117,16 +96,14 @@ export async function mobileStartXctestScreenRecording(fps, codec) {
|
|
|
117
96
|
this.assertFeatureEnabled(FEATURE_NAME);
|
|
118
97
|
}
|
|
119
98
|
|
|
120
|
-
const opts = {};
|
|
99
|
+
const opts: {codec?: number; fps?: number} = {};
|
|
121
100
|
if (_.isInteger(codec)) {
|
|
122
101
|
opts.codec = codec;
|
|
123
102
|
}
|
|
124
103
|
if (_.isInteger(fps)) {
|
|
125
104
|
opts.fps = fps;
|
|
126
105
|
}
|
|
127
|
-
const response =
|
|
128
|
-
await this.proxyCommand('/wda/video/start', 'POST', opts)
|
|
129
|
-
);
|
|
106
|
+
const response = await this.proxyCommand('/wda/video/start', 'POST', opts) as XcTestScreenRecordingInfo;
|
|
130
107
|
this.log.info(`Started a new screen recording: ${JSON.stringify(response)}`);
|
|
131
108
|
return response;
|
|
132
109
|
}
|
|
@@ -134,13 +111,11 @@ export async function mobileStartXctestScreenRecording(fps, codec) {
|
|
|
134
111
|
/**
|
|
135
112
|
* Retrieves information about the current running screen recording.
|
|
136
113
|
* If no screen recording is running then `null` is returned.
|
|
137
|
-
*
|
|
138
|
-
* @returns {Promise<XcTestScreenRecordingInfo?>}
|
|
139
114
|
*/
|
|
140
|
-
export async function mobileGetXctestScreenRecordingInfo(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
115
|
+
export async function mobileGetXctestScreenRecordingInfo(
|
|
116
|
+
this: XCUITestDriver,
|
|
117
|
+
): Promise<XcTestScreenRecordingInfo | null> {
|
|
118
|
+
return (await this.proxyCommand('/wda/video', 'GET')) as XcTestScreenRecordingInfo | null;
|
|
144
119
|
}
|
|
145
120
|
|
|
146
121
|
/**
|
|
@@ -157,29 +132,37 @@ export async function mobileGetXctestScreenRecordingInfo() {
|
|
|
157
132
|
* on the device directly or by doing factory reset.
|
|
158
133
|
*
|
|
159
134
|
* @since Xcode 15/iOS 17
|
|
160
|
-
* @param
|
|
135
|
+
* @param remotePath - The path to the remote location, where the resulting video should be
|
|
161
136
|
* uploaded.
|
|
162
137
|
* The following protocols are supported: `http`, `https`, `ftp`. Null or empty
|
|
163
138
|
* string value (the default setting) means the content of resulting file
|
|
164
139
|
* should be encoded as Base64 and passed as the endpoint response value. An
|
|
165
140
|
* exception will be thrown if the generated media file is too big to fit into
|
|
166
141
|
* the available process memory.
|
|
167
|
-
* @param
|
|
142
|
+
* @param user - The name of the user for the remote authentication.
|
|
168
143
|
* Only works if `remotePath` is provided.
|
|
169
|
-
* @param
|
|
144
|
+
* @param pass - The password for the remote authentication.
|
|
170
145
|
* Only works if `remotePath` is provided.
|
|
171
|
-
* @param
|
|
172
|
-
* @param
|
|
146
|
+
* @param headers - Additional headers mapping for multipart http(s) uploads
|
|
147
|
+
* @param fileFieldName - The name of the form field where the file content BLOB should be stored for
|
|
173
148
|
* http(s) uploads
|
|
174
|
-
* @param
|
|
175
|
-
* @param
|
|
149
|
+
* @param formFields - Additional form fields for multipart http(s) uploads
|
|
150
|
+
* @param method - The http multipart upload method name.
|
|
176
151
|
* Only works if `remotePath` is provided.
|
|
177
|
-
* @returns
|
|
152
|
+
* @returns The resulting movie with base64-encoded content or empty string if uploaded remotely.
|
|
178
153
|
* @throws {Error} If there was an error while retrieving the video
|
|
179
154
|
* file or the file content cannot be uploaded to the remote location.
|
|
180
|
-
* @this {XCUITestDriver}
|
|
181
155
|
*/
|
|
182
|
-
export async function mobileStopXctestScreenRecording(
|
|
156
|
+
export async function mobileStopXctestScreenRecording(
|
|
157
|
+
this: XCUITestDriver,
|
|
158
|
+
remotePath?: string,
|
|
159
|
+
user?: string,
|
|
160
|
+
pass?: string,
|
|
161
|
+
headers?: HTTPHeaders,
|
|
162
|
+
fileFieldName?: string,
|
|
163
|
+
formFields?: Record<string, any> | [string, any][],
|
|
164
|
+
method: 'PUT' | 'POST' | 'PATCH' = 'PUT',
|
|
165
|
+
): Promise<XcTestScreenRecording> {
|
|
183
166
|
const screenRecordingInfo = await this.mobileGetXctestScreenRecordingInfo();
|
|
184
167
|
if (!screenRecordingInfo) {
|
|
185
168
|
throw new Error('There is no active screen recording. Did you start one beforehand?');
|
|
@@ -187,8 +170,11 @@ export async function mobileStopXctestScreenRecording(remotePath, user, pass, he
|
|
|
187
170
|
|
|
188
171
|
this.log.debug(`Stopping the active screen recording: ${JSON.stringify(screenRecordingInfo)}`);
|
|
189
172
|
await this.proxyCommand('/wda/video/stop', 'POST', {});
|
|
190
|
-
const videoPath = await retrieveXcTestScreenRecording.
|
|
191
|
-
const result =
|
|
173
|
+
const videoPath = await retrieveXcTestScreenRecording.call(this, screenRecordingInfo.uuid);
|
|
174
|
+
const result: XcTestScreenRecording = {
|
|
175
|
+
...screenRecordingInfo,
|
|
176
|
+
payload: '', // Will be set below
|
|
177
|
+
};
|
|
192
178
|
try {
|
|
193
179
|
result.payload = await encodeBase64OrUpload(videoPath, remotePath, {
|
|
194
180
|
user, pass, headers, fileFieldName, formFields, method
|
|
@@ -199,6 +185,3 @@ export async function mobileStopXctestScreenRecording(remotePath, user, pass, he
|
|
|
199
185
|
return result;
|
|
200
186
|
}
|
|
201
187
|
|
|
202
|
-
/**
|
|
203
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
204
|
-
*/
|
package/lib/driver.ts
CHANGED
|
@@ -121,7 +121,7 @@ import type { PerfRecorder } from './commands/performance';
|
|
|
121
121
|
import type { AudioRecorder } from './commands/record-audio';
|
|
122
122
|
import type { TrafficCapture } from './commands/pcap';
|
|
123
123
|
import type { ScreenRecorder } from './commands/recordscreen';
|
|
124
|
-
import type { DVTServiceWithConnection } from '
|
|
124
|
+
import type { DVTServiceWithConnection } from 'appium-ios-remotexpc';
|
|
125
125
|
import type { IOSDeviceLog } from './device/log/ios-device-log';
|
|
126
126
|
import type { IOSSimulatorLog } from './device/log/ios-simulator-log';
|
|
127
127
|
import type { IOSCrashLog } from './device/log/ios-crash-log';
|