appium-mac2-driver
Version:
XCTest-based Appium driver for macOS apps automation
282 lines (244 loc) • 9.2 kB
text/typescript
import _ from 'lodash';
import type {
RouteMatcher,
HTTPMethod,
HTTPBody,
DefaultCreateSessionResult,
DriverData,
InitialOpts,
StringRecord,
ExternalDriver,
DriverCaps,
DriverOpts,
W3CDriverCaps,
} from '@appium/types';
import {BaseDriver, DeviceSettings} from 'appium/driver';
import {WDA_MAC_SERVER, type WDAMacServer} from './wda-mac';
import MAC2_CONSTRAINTS, {type Mac2Constraints} from './constraints';
import * as appManagemenetCommands from './commands/app-management';
import * as appleScriptCommands from './commands/applescript';
import * as executeCommands from './commands/execute';
import * as findCommands from './commands/find';
import * as gesturesCommands from './commands/gestures';
import * as navigationCommands from './commands/navigation';
import * as recordScreenCommands from './commands/record-screen';
import * as screenshotCommands from './commands/screenshots';
import * as sourceCommands from './commands/source';
import * as clipboardCommands from './commands/clipboard';
import * as nativeScreenRecordingCommands from './commands/native-record-screen';
import log from './logger';
import {newMethodMap} from './method-map';
import {executeMethodMap} from './execute-method-map';
const NO_PROXY: RouteMatcher[] = [
['GET', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/element/[^/]+/elements?$')],
['POST', new RegExp('^/session/[^/]+/elements?$')],
['POST', new RegExp('^/session/[^/]+/execute')],
['POST', new RegExp('^/session/[^/]+/execute/sync')],
['GET', new RegExp('^/session/[^/]+/timeouts$')],
['POST', new RegExp('^/session/[^/]+/timeouts$')],
];
export class Mac2Driver
extends BaseDriver<Mac2Constraints, StringRecord>
implements ExternalDriver<Mac2Constraints, string, StringRecord>
{
private isProxyActive: boolean;
private _wda: WDAMacServer | null;
_videoChunksBroadcaster: nativeScreenRecordingCommands.NativeVideoChunksBroadcaster;
_screenRecorder: recordScreenCommands.ScreenRecorder | null;
public proxyReqRes: (...args: any) => any;
static newMethodMap = newMethodMap;
static executeMethodMap = executeMethodMap;
constructor(opts: InitialOpts = {} as InitialOpts) {
super(opts);
this.desiredCapConstraints = _.cloneDeep(MAC2_CONSTRAINTS);
this.locatorStrategies = [
'id',
'name',
'accessibility id',
'xpath',
'class name',
'-ios predicate string',
'predicate string',
'-ios class chain',
'class chain',
];
this.resetState();
this.settings = new DeviceSettings({}, this.onSettingsUpdate.bind(this));
}
async onSettingsUpdate(key: string, value: unknown): Promise<void> {
if (!this._wda) {
return;
}
return await this._wda.proxy.command('/appium/settings', 'POST', {
settings: {[key]: value},
});
}
get wda(): WDAMacServer {
if (!this._wda) {
throw new Error('WDA server is not initialized');
}
return this._wda;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override proxyActive(sessionId: string): boolean {
return this.isProxyActive;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override getProxyAvoidList(sessionId: string): RouteMatcher[] {
return NO_PROXY;
}
override canProxy(): boolean {
return true;
}
async proxyCommand(url: string, method: HTTPMethod, body: HTTPBody = null): Promise<any> {
if (!this._wda) {
throw new Error('WDA server is not initialized');
}
return await this._wda.proxy.command(url, method, body);
}
override async getStatus(): Promise<any> {
if (!this._wda) {
throw new Error('WDA server is not initialized');
}
return await this._wda.proxy.command('/status', 'GET');
}
// needed to make image plugin work
async getWindowRect(): Promise<any> {
if (!this._wda) {
throw new Error('WDA server is not initialized');
}
return await this._wda.proxy.command('/window/rect', 'GET');
}
override async createSession(
w3cCaps1: W3CMac2DriverCaps,
w3cCaps2?: W3CMac2DriverCaps,
w3cCaps3?: W3CMac2DriverCaps,
driverData?: DriverData[],
): Promise<DefaultCreateSessionResult<Mac2Constraints>> {
const [sessionId, caps] = await super.createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData);
this._wda = WDA_MAC_SERVER;
this.caps = caps as Mac2DriverCaps;
this.opts = this.opts as Mac2DriverOpts;
try {
const prerun = caps.prerun as PrerunCapability | undefined;
if (prerun) {
if (!_.isString(prerun.command) && !_.isString(prerun.script)) {
throw new Error(
`'prerun' capability value must either contain ` +
`'script' or 'command' entry of string type`,
);
}
log.info('Executing prerun AppleScript');
const output = await this.macosExecAppleScript(prerun.script, undefined, prerun.command);
if (_.trim(output)) {
log.info(`Prerun script output: ${output}`);
}
}
await this._wda.startSession(caps, {
reqBasePath: this.basePath,
});
} catch (e: any) {
await this.deleteSession();
throw e;
}
this.proxyReqRes = this.wda.proxy.proxyReqRes.bind(this._wda.proxy);
this.isProxyActive = true;
return [sessionId, caps];
}
override async deleteSession(): Promise<void> {
await this._screenRecorder?.stop(true);
if (this._videoChunksBroadcaster.hasPublishers) {
if (this._wda) {
try {
await this.wda.proxy.command('/wda/video/stop', 'POST', {});
} catch {}
}
await this._videoChunksBroadcaster.shutdown(5000);
}
if (this._wda) {
await this.wda.stopSession();
}
const postrun = this.opts.postrun as PostrunCapability | undefined;
if (postrun) {
if (!_.isString(postrun.command) && !_.isString(postrun.script)) {
log.error(
`'postrun' capability value must either contain ` +
`'script' or 'command' entry of string type`,
);
} else {
log.info('Executing postrun AppleScript');
try {
const output = await this.macosExecAppleScript(
postrun.script,
undefined,
postrun.command,
);
if (_.trim(output)) {
log.info(`Postrun script output: ${output}`);
}
} catch (e: any) {
log.error(e.message);
}
}
}
this.resetState();
await super.deleteSession();
}
private resetState(): void {
this._wda = null;
this.isProxyActive = false;
this._videoChunksBroadcaster = new nativeScreenRecordingCommands.NativeVideoChunksBroadcaster(
this.eventEmitter,
this.log,
);
this._screenRecorder = null;
}
macosLaunchApp = appManagemenetCommands.macosLaunchApp;
macosActivateApp = appManagemenetCommands.macosActivateApp;
macosTerminateApp = appManagemenetCommands.macosTerminateApp;
macosQueryAppState = appManagemenetCommands.macosQueryAppState;
macosExecAppleScript = appleScriptCommands.macosExecAppleScript;
execute = executeCommands.execute;
findElOrEls = findCommands.findElOrEls;
macosSetValue = gesturesCommands.macosSetValue;
macosClick = gesturesCommands.macosClick;
macosScroll = gesturesCommands.macosScroll;
macosSwipe = gesturesCommands.macosSwipe;
macosRightClick = gesturesCommands.macosRightClick;
macosHover = gesturesCommands.macosHover;
macosDoubleClick = gesturesCommands.macosDoubleClick;
macosClickAndDrag = gesturesCommands.macosClickAndDrag;
macosClickAndDragAndHold = gesturesCommands.macosClickAndDragAndHold;
macosKeys = gesturesCommands.macosKeys;
macosPressAndHold = gesturesCommands.macosPressAndHold;
macosTap = gesturesCommands.macosTap;
macosDoubleTap = gesturesCommands.macosDoubleTap;
macosPressAndDrag = gesturesCommands.macosPressAndDrag;
macosPressAndDragAndHold = gesturesCommands.macosPressAndDragAndHold;
macosGetClipboard = clipboardCommands.macosGetClipboard;
macosSetClipboard = clipboardCommands.macosSetClipboard;
macosDeepLink = navigationCommands.macosDeepLink;
startRecordingScreen = recordScreenCommands.startRecordingScreen;
stopRecordingScreen = recordScreenCommands.stopRecordingScreen;
macosStartNativeScreenRecording = nativeScreenRecordingCommands.macosStartNativeScreenRecording;
macosGetNativeScreenRecordingInfo =
nativeScreenRecordingCommands.macosGetNativeScreenRecordingInfo;
macosStopNativeScreenRecording = nativeScreenRecordingCommands.macosStopNativeScreenRecording;
macosListDisplays = nativeScreenRecordingCommands.macosListDisplays;
macosScreenshots = screenshotCommands.macosScreenshots;
macosSource = sourceCommands.macosSource;
}
export default Mac2Driver;
interface PrerunCapability {
command?: string;
script?: string;
}
interface PostrunCapability {
command?: string;
script?: string;
}
type Mac2DriverOpts = DriverOpts<Mac2Constraints>;
type Mac2DriverCaps = DriverCaps<Mac2Constraints>;
type W3CMac2DriverCaps = W3CDriverCaps<Mac2Constraints>;