UNPKG

appium-flutter-driver

Version:
289 lines (249 loc) 10.8 kB
// @ts-ignore: no 'errors' export module import _ from 'lodash'; import { BaseDriver } from 'appium/driver'; import { log as logger } from './logger'; import { executeElementCommand, executeGetVMCommand, executeGetIsolateCommand } from './sessions/observatory'; import { PLATFORM } from './platform'; import { createSession, reConnectFlutterDriver } from './sessions/session'; import { driverShouldDoProxyCmd, FLUTTER_CONTEXT_NAME, getContexts, getCurrentContext, NATIVE_CONTEXT_NAME, setContext } from './commands/context'; import { clear, getText, setValue } from './commands/element'; import { execute } from './commands/execute'; import { click, longTap, performTouch, tap, tapEl } from './commands/gesture'; import { getScreenshot } from './commands/screen'; import { getClipboard, setClipboard } from './commands/clipboard'; import { desiredCapConstraints } from './desired-caps'; import { XCUITestDriver } from 'appium-xcuitest-driver'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; import type { DefaultCreateSessionResult, DriverCaps, DriverData, W3CDriverCaps, RouteMatcher, Orientation } from '@appium/types'; import type { IsolateSocket } from './sessions/isolate_socket'; import type { Server } from 'node:net'; import type { LogMonitor } from './sessions/log-monitor'; type FluttertDriverConstraints = typeof desiredCapConstraints; // Need to not proxy in WebView context const WEBVIEW_NO_PROXY = [ [`GET`, new RegExp(`^/session/[^/]+/appium`)], [`GET`, new RegExp(`^/session/[^/]+/context`)], [`GET`, new RegExp(`^/session/[^/]+/element/[^/]+/rect`)], [`GET`, new RegExp(`^/session/[^/]+/log/types$`)], [`GET`, new RegExp(`^/session/[^/]+/orientation`)], [`POST`, new RegExp(`^/session/[^/]+/appium`)], [`POST`, new RegExp(`^/session/[^/]+/context`)], [`POST`, new RegExp(`^/session/[^/]+/log$`)], [`POST`, new RegExp(`^/session/[^/]+/orientation`)], [`POST`, new RegExp(`^/session/[^/]+/touch/multi/perform`)], [`POST`, new RegExp(`^/session/[^/]+/touch/perform`)], ] as import('@appium/types').RouteMatcher[]; class FlutterDriver extends BaseDriver<FluttertDriverConstraints> { public socket: IsolateSocket | null; public locatorStrategies = [`key`, `css selector`]; public proxydriver: XCUITestDriver | AndroidUiautomator2Driver | null; public device: any; public portForwardLocalPort: string | null; public localServer: Server | null; protected _logmon: LogMonitor | null; // Used to keep the capabilities internally public internalCaps: DriverCaps<FluttertDriverConstraints>; public receiveAsyncResponse: (...args: any[]) => Promise<any>; // to handle WebView context public proxyWebViewActive = false; // session public executeElementCommand = executeElementCommand; public executeGetVMCommand = executeGetVMCommand; public executeGetIsolateCommand = executeGetIsolateCommand; public execute = execute; public executeAsync = execute; // element public getText = getText; public setValue = setValue; public clear = clear; public getScreenshot = getScreenshot; // gesture public click = click; public longTap = longTap; public tapEl = tapEl; public tap = tap; public performTouch = performTouch; // context public getContexts = getContexts; public getCurrentContext = getCurrentContext; public setContext = setContext; protected currentContext = FLUTTER_CONTEXT_NAME; private driverShouldDoProxyCmd = driverShouldDoProxyCmd; // content public getClipboard = getClipboard; public setClipboard = setClipboard; constructor(opts, shouldValidateCaps: boolean) { super(opts, shouldValidateCaps); this.socket = null; this.device = null; this._logmon = null; this.desiredCapConstraints = desiredCapConstraints; // Used to keep the port for port forward to clear the pair. this.portForwardLocalPort = null; // Used for iOS to end the local server to proxy the request. this.localServer = null; } public async createSession(...args): Promise<DefaultCreateSessionResult<FluttertDriverConstraints>> { const [sessionId, caps] = await super.createSession(...JSON.parse(JSON.stringify(args)) as [ W3CDriverCaps, W3CDriverCaps, W3CDriverCaps, DriverData[] ]); this.internalCaps = caps; return createSession.bind(this)(sessionId, caps, ...JSON.parse(JSON.stringify(args))); } public async deleteSession() { this.log.info(`Deleting Flutter Driver session`); this._logmon?.stop(); this._logmon = null; this.proxydriver?.eventEmitter?.removeAllListeners('syslogStarted'); this.log.info('Cleanup the port forward'); switch (_.toLower(this.internalCaps.platformName)) { case PLATFORM.IOS: this.localServer?.close(); this.localServer = null; break; case PLATFORM.ANDROID: if (this.portForwardLocalPort) { if (this.proxydriver) { await (this.proxydriver as AndroidUiautomator2Driver).adb?.removePortForward(this.portForwardLocalPort); } } break; } if (this.proxydriver) { this.log.info('Deleting the proxy driver session.'); try { await this.proxydriver.deleteSession(this.sessionId); } catch (e) { this.log.warn(e.message); } this.proxydriver = null; } await super.deleteSession(); } public async installApp(appPath: string, opts = {}) { // @ts-expect-error this exist in xcuitestdriver or uia2 driver this.proxydriver?.installApp(appPath, opts); } public async activateApp(appId: string) { // @ts-expect-error this exist in xcuitestdriver or uia2 driver this.proxydriver?.activateApp(appId); await reConnectFlutterDriver.bind(this)(this.internalCaps); } public async terminateApp(appId: string) { // @ts-expect-error this exist in xcuitestdriver or uia2 driver return await this.proxydriver?.terminateApp(appId); } public async back() { // @ts-expect-error this exist in xcuitestdriver or uia2 driver return await this.proxydriver?.back(); } public async getOrientation(): Promise<string|null> { if (!this.proxydriver) { return null; } switch (_.toLower(this.internalCaps.platformName)) { case PLATFORM.IOS: return await (this.proxydriver as XCUITestDriver).proxyCommand('/orientation', 'GET'); default: return await (this.proxydriver as AndroidUiautomator2Driver).getOrientation(); } } public async setOrientation(orientation: string) { switch (_.toLower(this.internalCaps.platformName)) { case PLATFORM.IOS: return await (this.proxydriver as XCUITestDriver).proxyCommand('/orientation', 'POST', {orientation}); default: return await (this.proxydriver as AndroidUiautomator2Driver).setOrientation(orientation as Orientation); } } public validateLocatorStrategy(strategy: string) { // @todo refactor DRY if (this.currentContext === `NATIVE_APP`) { return this.proxydriver?.validateLocatorStrategy(strategy); } super.validateLocatorStrategy(strategy, false); } validateDesiredCaps(caps: DriverCaps<FluttertDriverConstraints>): caps is DriverCaps<FluttertDriverConstraints> { // check with the base class, and return if it fails const res = super.validateDesiredCaps(caps); if (!res) { return res; } // finally, return true since the superclass check passed, as did this return true; } public async proxyCommand (url: string, method: string, body = null) { // @ts-expect-error this exist in xcuitestdriver or uia2 driver const result = await this.proxydriver?.proxyCommand(url, method, body); return result; } public async executeCommand(cmd: string, ...args: [string, [{skipAttachObservatoryUrl: string, any: any}]]) { if (new RegExp(/^[\s]*mobile:[\s]*activateApp$/).test(args[0])) { const { skipAttachObservatoryUrl = false } = args[1][0]; await this.proxydriver?.executeCommand(cmd, ...args); if (skipAttachObservatoryUrl) { return; } await reConnectFlutterDriver.bind(this)(this.internalCaps); return; } else if (new RegExp(/^[\s]*mobile:[\s]*terminateApp$/).test(args[0])) { // to make the behavior as same as this.terminateApp return await this.proxydriver?.executeCommand(cmd, ...args); } else if (cmd === `receiveAsyncResponse`) { logger.debug(`Executing FlutterDriver response '${cmd}'`); return await this.receiveAsyncResponse(...args); } else if ([`setOrientation`, `getOrientation`, `back`].includes(cmd)) { // The `setOrientation` and `getOrientation` commands are handled differently // for iOS and Android platforms. These commands are deferred to the base driver's // implementation (`super.executeCommand`) to ensure compatibility with both platforms // and to leverage the platform-specific logic already implemented in the base driver. logger.debug(`Executing FlutterDriver command '${cmd}'`); return await super.executeCommand(cmd, ...args); } else { if (this.driverShouldDoProxyCmd(cmd)) { logger.debug(`Executing proxied driver command '${cmd}'`); // There are 2 CommandTimeout (FlutterDriver and proxy) // Only FlutterDriver CommandTimeout is used; Proxy is disabled // All proxy commands needs to reset the FlutterDriver CommandTimeout // Here we manually reset the FlutterDriver CommandTimeout for commands that goes to proxy. this.clearNewCommandTimeout(); const result = await this.proxydriver?.executeCommand(cmd, ...args); this.startNewCommandTimeout(); return result; } else { logger.debug(`Executing Flutter driver command '${cmd}'`); return await super.executeCommand(cmd, ...args); } } } public getProxyAvoidList(): RouteMatcher[] { if ([FLUTTER_CONTEXT_NAME, NATIVE_CONTEXT_NAME].includes(this.currentContext)) { return []; } return WEBVIEW_NO_PROXY; } public proxyActive(): boolean { // In WebView context, all request should got to each driver // so that they can handle http request properly. // On iOS, WebView context is handled by XCUITest driver while Android is by chromedriver. // It means XCUITest driver should keep the XCUITest driver as a proxy, // while UIAutomator2 driver should proxy to chromedriver instead of UIA2 proxy. return this.proxyWebViewActive && this.proxydriver?.constructor.name !== XCUITestDriver.name; } public canProxy(): boolean { // As same as proxyActive, all request should got to each driver // so that they can handle http request properly return this.proxyWebViewActive; } } export { FlutterDriver };