appium-xcuitest-driver 10.14.0 → 10.14.2
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/execute.d.ts +10 -22
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +12 -28
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/location.d.ts +8 -11
- package/build/lib/commands/location.d.ts.map +1 -1
- package/build/lib/commands/location.js +7 -15
- package/build/lib/commands/location.js.map +1 -1
- package/build/lib/commands/navigation.d.ts +14 -26
- package/build/lib/commands/navigation.d.ts.map +1 -1
- package/build/lib/commands/navigation.js +10 -18
- package/build/lib/commands/navigation.js.map +1 -1
- package/build/lib/commands/pcap.d.ts +18 -38
- package/build/lib/commands/pcap.d.ts.map +1 -1
- package/build/lib/commands/pcap.js +9 -14
- package/build/lib/commands/pcap.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/screenshots.d.ts +15 -9
- package/build/lib/commands/screenshots.d.ts.map +1 -1
- package/build/lib/commands/screenshots.js +13 -11
- package/build/lib/commands/screenshots.js.map +1 -1
- package/build/lib/commands/source.d.ts +10 -8
- package/build/lib/commands/source.d.ts.map +1 -1
- package/build/lib/commands/source.js +11 -14
- package/build/lib/commands/source.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/driver.js +1 -1
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +0 -6
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/{condition.js → condition.ts} +21 -77
- package/lib/commands/{execute.js → execute.ts} +41 -37
- package/lib/commands/{location.js → location.ts} +19 -22
- package/lib/commands/{navigation.js → navigation.ts} +23 -26
- package/lib/commands/{pcap.js → pcap.ts} +28 -28
- package/lib/commands/{record-audio.js → record-audio.ts} +35 -33
- package/lib/commands/{screenshots.js → screenshots.ts} +24 -16
- package/lib/commands/{source.js → source.ts} +23 -20
- package/lib/commands/types.ts +63 -0
- package/lib/commands/{xctest-record-screen.js → xctest-record-screen.ts} +54 -71
- package/lib/driver.ts +2 -2
- package/lib/execute-method-map.ts +0 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
*/
|
|
@@ -2,19 +2,23 @@ import {retryInterval} from 'asyncbox';
|
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import {errors} from 'appium/driver';
|
|
4
4
|
import {util, imageUtil} from 'appium/support';
|
|
5
|
+
import type {XCUITestDriver} from '../driver';
|
|
6
|
+
import type {Simulator} from 'appium-ios-simulator';
|
|
7
|
+
import type {Element} from '@appium/types';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
10
|
+
* Takes a screenshot of the current screen.
|
|
11
|
+
*
|
|
12
|
+
* @returns Base64-encoded screenshot data
|
|
9
13
|
*/
|
|
10
|
-
export async function getScreenshot() {
|
|
14
|
+
export async function getScreenshot(this: XCUITestDriver): Promise<string> {
|
|
11
15
|
if (this.isWebContext()) {
|
|
12
16
|
const webScreenshotMode = (await this.settings.getSettings()).webScreenshotMode;
|
|
13
17
|
switch (_.toLower(webScreenshotMode)) {
|
|
14
18
|
case 'page':
|
|
15
19
|
case 'viewport':
|
|
16
20
|
return await this.remote.captureScreenshot({
|
|
17
|
-
coordinateSystem:
|
|
21
|
+
coordinateSystem: _.capitalize(webScreenshotMode) as 'Viewport' | 'Page',
|
|
18
22
|
});
|
|
19
23
|
case 'native':
|
|
20
24
|
case undefined:
|
|
@@ -29,7 +33,7 @@ export async function getScreenshot() {
|
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
const getScreenshotFromWDA = async () => {
|
|
36
|
+
const getScreenshotFromWDA = async (): Promise<string> => {
|
|
33
37
|
this.log.debug(`Taking screenshot with WDA`);
|
|
34
38
|
const data = await this.proxyCommand('/screenshot', 'GET');
|
|
35
39
|
if (!_.isString(data)) {
|
|
@@ -53,14 +57,14 @@ export async function getScreenshot() {
|
|
|
53
57
|
|
|
54
58
|
try {
|
|
55
59
|
return await getScreenshotFromWDA();
|
|
56
|
-
} catch (err) {
|
|
60
|
+
} catch (err: any) {
|
|
57
61
|
this.log.warn(`Error getting screenshot: ${err.message}`);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
// simulator attempt
|
|
61
65
|
if (this.isSimulator()) {
|
|
62
66
|
this.log.info(`Falling back to 'simctl io screenshot' API`);
|
|
63
|
-
const payload = await
|
|
67
|
+
const payload = await (this.device as Simulator).simctl.getScreenshot();
|
|
64
68
|
if (!payload) {
|
|
65
69
|
throw new errors.UnableToCaptureScreen();
|
|
66
70
|
}
|
|
@@ -68,13 +72,19 @@ export async function getScreenshot() {
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// Retry for real devices only. Fail fast on Simulator if simctl does not work as expected
|
|
71
|
-
return
|
|
75
|
+
return await retryInterval(2, 1000, getScreenshotFromWDA) as string;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
|
-
*
|
|
79
|
+
* Takes a screenshot of a specific element.
|
|
80
|
+
*
|
|
81
|
+
* @param el - Element to capture
|
|
82
|
+
* @returns Base64-encoded screenshot data
|
|
76
83
|
*/
|
|
77
|
-
export async function getElementScreenshot(
|
|
84
|
+
export async function getElementScreenshot(
|
|
85
|
+
this: XCUITestDriver,
|
|
86
|
+
el: Element<string> | string,
|
|
87
|
+
): Promise<string> {
|
|
78
88
|
el = util.unwrapElement(el);
|
|
79
89
|
if (this.isWebContext()) {
|
|
80
90
|
const atomsElement = this.getAtomsElement(el);
|
|
@@ -96,10 +106,11 @@ export async function getElementScreenshot(el) {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
109
|
+
* Takes a screenshot of the current viewport.
|
|
110
|
+
*
|
|
111
|
+
* @returns Base64-encoded screenshot data
|
|
101
112
|
*/
|
|
102
|
-
export async function getViewportScreenshot() {
|
|
113
|
+
export async function getViewportScreenshot(this: XCUITestDriver): Promise<string> {
|
|
103
114
|
if (this.isWebContext()) {
|
|
104
115
|
return await this.remote.captureScreenshot();
|
|
105
116
|
}
|
|
@@ -130,6 +141,3 @@ export async function getViewportScreenshot() {
|
|
|
130
141
|
return await imageUtil.cropBase64Image(screenshot, region);
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
/**
|
|
134
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
135
|
-
*/
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import js2xml from 'js2xmlparser2';
|
|
3
|
+
import type {XCUITestDriver} from '../driver';
|
|
4
|
+
import type {SourceFormat} from './types';
|
|
3
5
|
|
|
4
6
|
const APPIUM_AUT_TAG = 'AppiumAUT';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Retrieves the page source of the current application.
|
|
10
|
+
*
|
|
11
|
+
* @returns The page source as XML or HTML string
|
|
8
12
|
*/
|
|
9
|
-
export async function getPageSource() {
|
|
13
|
+
export async function getPageSource(this: XCUITestDriver): Promise<string> {
|
|
10
14
|
if (this.isWebContext()) {
|
|
11
15
|
const script = 'return document.documentElement.outerHTML';
|
|
12
16
|
return await this.executeAtom('execute_script', [script, []]);
|
|
@@ -28,14 +32,17 @@ export async function getPageSource() {
|
|
|
28
32
|
/**
|
|
29
33
|
* Retrieve the source tree of the current page in XML or JSON format.
|
|
30
34
|
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param
|
|
35
|
+
* @param format - Page tree source representation format.
|
|
36
|
+
* @param excludedAttributes - A comma-separated string of attribute names to exclude from the output. Only works if `format` is `xml`.
|
|
33
37
|
* @privateRemarks Why isn't `excludedAttributes` an array?
|
|
34
|
-
* @returns
|
|
35
|
-
* @this {XCUITestDriver}
|
|
38
|
+
* @returns The source tree of the current page in the given format.
|
|
36
39
|
*/
|
|
37
|
-
export async function mobileGetSource(
|
|
38
|
-
|
|
40
|
+
export async function mobileGetSource(
|
|
41
|
+
this: XCUITestDriver,
|
|
42
|
+
format: SourceFormat = 'xml',
|
|
43
|
+
excludedAttributes?: string,
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
const paramsMap: Record<string, string> = {
|
|
39
46
|
format,
|
|
40
47
|
scope: APPIUM_AUT_TAG,
|
|
41
48
|
};
|
|
@@ -45,7 +52,7 @@ export async function mobileGetSource(format = 'xml', excludedAttributes) {
|
|
|
45
52
|
const query = Object.entries(paramsMap)
|
|
46
53
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
47
54
|
.join('&');
|
|
48
|
-
return
|
|
55
|
+
return await this.proxyCommand(`/source?${query}`, 'GET') as string;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/**
|
|
@@ -76,15 +83,14 @@ export async function mobileGetSource(format = 'xml', excludedAttributes) {
|
|
|
76
83
|
* rawIdentifier: null }
|
|
77
84
|
* ```
|
|
78
85
|
*/
|
|
79
|
-
function getTreeForXML(srcTree) {
|
|
80
|
-
function getTree(element, elementIndex, parentPath) {
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
function getTreeForXML(srcTree: any): any {
|
|
87
|
+
function getTree(element: any, elementIndex: number, parentPath: string): any {
|
|
88
|
+
const curPath = `${parentPath}/${elementIndex}`;
|
|
89
|
+
const rect = element.rect || {};
|
|
83
90
|
/**
|
|
84
91
|
* @privateRemarks I don't even want to try to type this right now
|
|
85
|
-
* @type {any}
|
|
86
92
|
*/
|
|
87
|
-
|
|
93
|
+
const subtree: any = {
|
|
88
94
|
'@': {
|
|
89
95
|
type: `XCUIElementType${element.type}`,
|
|
90
96
|
enabled: parseInt(element.isEnabled, 10) === 1,
|
|
@@ -114,11 +120,11 @@ function getTreeForXML(srcTree) {
|
|
|
114
120
|
[`XCUIElementType${element.type}`]: subtree,
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
|
-
|
|
123
|
+
const tree = getTree(srcTree, 0, '');
|
|
118
124
|
return tree;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
function getSourceXml(jsonSource) {
|
|
127
|
+
function getSourceXml(jsonSource: any): string {
|
|
122
128
|
return js2xml('AppiumAUT', jsonSource, {
|
|
123
129
|
wrapArray: {enabled: false, elementName: 'element'},
|
|
124
130
|
declaration: {include: true},
|
|
@@ -126,6 +132,3 @@ function getSourceXml(jsonSource) {
|
|
|
126
132
|
});
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
/**
|
|
130
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
131
|
-
*/
|
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';
|
|
@@ -500,7 +500,7 @@ export class XCUITestDriver
|
|
|
500
500
|
this.log.debug(`Executing command '${cmd}'`);
|
|
501
501
|
|
|
502
502
|
if (cmd === 'receiveAsyncResponse') {
|
|
503
|
-
return await this.receiveAsyncResponse(
|
|
503
|
+
return await this.receiveAsyncResponse(args[0], args[1]);
|
|
504
504
|
}
|
|
505
505
|
// TODO: once this fix gets into base driver remove from here
|
|
506
506
|
if (cmd === 'getStatus') {
|