UNPKG

appium-flutter-driver

Version:
245 lines (207 loc) 8.89 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 } from '@appium/types'; import type { IsolateSocket } from './sessions/isolate_socket'; import type { Server } from 'node:net'; 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; public device: any; public portForwardLocalPort: string | null; public localServer: Server | 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.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.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) { await this.proxydriver.adb.removePortForward(this.portForwardLocalPort); } break; } if (this.proxydriver) { this.log.info('Deleting the proxy driver session.'); try { await this.proxydriver.deleteSession(); } catch (e) { this.log.warn(e.message); } this.proxydriver = null; } await super.deleteSession(); } public async installApp(appPath: string, opts = {}) { this.proxydriver.installApp(appPath, opts); } public async activateApp(appId: string) { this.proxydriver.activateApp(appId); await reConnectFlutterDriver.bind(this)(this.internalCaps); } public async terminateApp(appId: string) { return await this.proxydriver.terminateApp(appId); } public async getOrientation(): Promise<string> { return await this.proxydriver.getOrientation(); } public async setOrientation(orientation: string) { return await this.proxydriver.setOrientation(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) { 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 (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 };