From 211617e91c2fe7b40c79b01fb98c1ddcc24a9025 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Wed, 17 Sep 2025 14:36:09 +0400 Subject: [PATCH 1/7] feat: Added support for macOS --- README.md | 5 ++++ package.json | 6 +++-- src/commands/element.ts | 51 ++++++++++++++++++++++---------------- src/desiredCaps.ts | 2 +- src/driver.ts | 7 +++++- src/macOS.ts | 30 ++++++++++++++++++++++ src/platform.ts | 1 + src/session.ts | 8 ++++++ src/utils.ts | 23 ++++++++++++++++- test/unit/element.specs.ts | 43 ++++++++++++++++++++++++++++++++ 10 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 src/macOS.ts diff --git a/README.md b/README.md index 61bd93a..11e99c1 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,11 @@ Get the latest version from `https://pub.dev/packages/appium_flutter_server/inst flutter build ipa --release integration_test/appium_test.dart ``` +7. Build the MacOS app: + ```bash + flutter build macos --release integration_test/appium_test.dart + ``` + Bingo! You are ready to run your tests using Appium Flutter Integration Driver. Check if your Flutter app is running on the device or emulator. diff --git a/package.json b/package.json index d945a1f..d3dbd67 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "appium", "flutter" ], - "version": "1.4.1", + "version": "1.5.0", "author": "", "license": "MIT License", "repository": { @@ -29,7 +29,8 @@ "automationName": "FlutterIntegration", "platformNames": [ "Android", - "iOS" + "iOS", + "Mac" ], "mainClass": "AppiumFlutterDriver", "flutterServerVersion": ">=0.0.18 <1.0.0" @@ -98,6 +99,7 @@ "appium-ios-device": "^2.7.20", "appium-uiautomator2-driver": "^4.1.5", "appium-xcuitest-driver": "9.1.2", + "appium-mac2-driver": "^2.2.2", "async-retry": "^1.3.3", "asyncbox": "^3.0.0", "bluebird": "^3.7.2", diff --git a/src/commands/element.ts b/src/commands/element.ts index 4b1a237..50c3ff5 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -1,7 +1,11 @@ import _ from 'lodash'; -import { getProxyDriver } from '../utils'; +import { getProxyDriver, FLUTTER_LOCATORS } from '../utils'; import { JWProxy } from 'appium/driver'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; +// @ts-ignore +import { XCUITestDriver } from 'appium-xcuitest-driver'; +// @ts-ignore +import { Mac2Driver } from 'appium-mac2-driver'; import { W3C_ELEMENT_KEY } from 'appium/driver'; import type { AppiumFlutterDriver } from '../driver'; @@ -15,28 +19,33 @@ export async function findElOrEls( ): Promise { const driver = await getProxyDriver.bind(this)(strategy); let elementBody; - if ( - !(driver instanceof JWProxy) && - !(this.proxydriver instanceof AndroidUiautomator2Driver) + function constructFindElementPayload( + strategy: string, + selector: string, + proxyDriver: XCUITestDriver | AndroidUiautomator2Driver | Mac2Driver, ) { - elementBody = { - using: strategy, - value: selector, - context, //this needs be validated - }; - } else { - elementBody = { - strategy, - selector: ['-flutter descendant', '-flutter ancestor'].includes( - strategy, - ) - ? _.isString(selector) - ? JSON.parse(selector) - : selector - : selector, - context, - }; + const isFlutterLocator = + strategy.startsWith('-flutter') || FLUTTER_LOCATORS.includes(strategy); + + const parsedSelector = + ['-flutter descendant', '-flutter ancestor'].includes(strategy) && + _.isString(selector) + ? JSON.parse(selector) + : selector; // Special case + + // If user is looking for Native IOS/Mac locator + if (!isFlutterLocator && (proxyDriver instanceof XCUITestDriver || proxyDriver instanceof Mac2Driver)) { + return { using: strategy, value: parsedSelector, context }; + } else { + return { strategy, selector: parsedSelector, context }; + } } + + elementBody = constructFindElementPayload( + strategy, + selector, + this.proxydriver, + ); if (mult) { const response = await driver.command('/elements', 'POST', elementBody); response.forEach((element: any) => { diff --git a/src/desiredCaps.ts b/src/desiredCaps.ts index b5d5430..c1d3880 100644 --- a/src/desiredCaps.ts +++ b/src/desiredCaps.ts @@ -7,7 +7,7 @@ export const desiredCapConstraints = { presence: true, }, platformName: { - inclusionCaseInsensitive: ['iOS', 'Android'], + inclusionCaseInsensitive: ['iOS', 'Android', 'Mac'], isString: true, presence: true, }, diff --git a/src/driver.ts b/src/driver.ts index 41f057d..9cc1a3a 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -10,6 +10,8 @@ type FlutterDriverConstraints = typeof desiredCapConstraints; // @ts-ignore import { XCUITestDriver } from 'appium-xcuitest-driver'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; +// @ts-ignore +import { Mac2Driver } from 'appium-mac2-driver'; import { createSession as createSessionMixin } from './session'; import { findElOrEls, @@ -55,7 +57,7 @@ const WEBVIEW_NO_PROXY = [ export class AppiumFlutterDriver extends BaseDriver { // @ts-ignore - public proxydriver: XCUITestDriver | AndroidUiautomator2Driver; + public proxydriver: XCUITestDriver | AndroidUiautomator2Driver | Mac2Driver; public flutterPort: number | null | undefined; private internalCaps: DriverCaps | undefined; public proxy: JWProxy | undefined; @@ -220,6 +222,9 @@ export class AppiumFlutterDriver extends BaseDriver { this.currentContext === this.NATIVE_CONTEXT_NAME && isFlutterDriverCommand(command) ) { + this.log.debug( + `executeCommand: command ${command} is flutter command using flutter driver`, + ); return await super.executeCommand(command, ...args); } else { this.log.info( diff --git a/src/macOS.ts b/src/macOS.ts new file mode 100644 index 0000000..41f2e18 --- /dev/null +++ b/src/macOS.ts @@ -0,0 +1,30 @@ +import type { AppiumFlutterDriver } from './driver'; +// @ts-ignore +import { Mac2Driver } from 'appium-mac2-driver'; +import type { InitialOpts } from '@appium/types'; +import { DEVICE_CONNECTIONS_FACTORY } from './iProxy'; + +export async function startMacOsSession( + this: AppiumFlutterDriver, + ...args: any[] +): Promise { + this.log.info(`Starting an MacOs proxy session`); + const macOsDriver = new Mac2Driver({} as InitialOpts); + await macOsDriver.createSession(...args); + return macOsDriver; +} + +export async function macOsPortForward( + udid: string, + systemPort: number, + devicePort: number, +) { + await DEVICE_CONNECTIONS_FACTORY.requestConnection(udid, systemPort, { + usePortForwarding: true, + devicePort: devicePort, + }); +} + +export function macOsRemovePortForward(udid: string, systemPort: number) { + DEVICE_CONNECTIONS_FACTORY.releaseConnection(udid, systemPort); +} diff --git a/src/platform.ts b/src/platform.ts index cd907cc..852ac74 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,4 +1,5 @@ export const PLATFORM = { IOS: 'ios', ANDROID: 'android', + MAC: 'mac', } as const; diff --git a/src/session.ts b/src/session.ts index 2e85cc4..8934706 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import { PLATFORM } from './platform'; import { startAndroidSession } from './android'; import { startIOSSession } from './iOS'; +import { startMacOsSession } from './macOS'; import type { DefaultCreateSessionResult } from '@appium/types'; export async function createSession( @@ -28,6 +29,13 @@ export async function createSession( this.proxydriver.denyInsecure = this.denyInsecure; this.proxydriver.allowInsecure = this.allowInsecure; break; + case PLATFORM.MAC: + this.proxydriver = await startMacOsSession.bind(this)(...args); + this.proxydriver.relaxedSecurityEnabled = + this.relaxedSecurityEnabled; + this.proxydriver.denyInsecure = this.denyInsecure; + this.proxydriver.allowInsecure = this.allowInsecure; + break; default: this.log.errorWithException( `Unsupported platformName: ${caps.platformName}. ` + diff --git a/src/utils.ts b/src/utils.ts index aeb0f09..89d3899 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,8 @@ import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; +// @ts-ignore +import { XCUITestDriver } from 'appium-xcuitest-driver'; +// @ts-ignore +import { Mac2Driver } from 'appium-mac2-driver'; import { findAPortNotInUse } from 'portscanner'; import { waitForCondition } from 'asyncbox'; import { JWProxy } from '@appium/base-driver'; @@ -23,19 +27,36 @@ export const FLUTTER_LOCATORS = [ 'text', 'type', 'text containing', + 'descendant', + 'ancestor', ]; export async function getProxyDriver( this: AppiumFlutterDriver, strategy: string, ): Promise { if (strategy.startsWith('-flutter') || FLUTTER_LOCATORS.includes(strategy)) { + this.log.debug( + `getProxyDriver: using flutter driver, strategy: ${strategy}`, + ); return this.proxy; } else if (this.proxydriver instanceof AndroidUiautomator2Driver) { + this.log.debug( + 'getProxyDriver: using AndroidUiautomator2Driver driver for Android', + ); // @ts-ignore Proxy instance is OK return this.proxydriver.uiautomator2.jwproxy; - } else { + } else if (this.proxydriver instanceof XCUITestDriver) { + this.log.debug('getProxyDriver: using XCUITestDriver driver for iOS'); // @ts-ignore Proxy instance is OK return this.proxydriver.wda.jwproxy; + } else if (this.proxydriver instanceof Mac2Driver) { + this.log.debug('getProxyDriver: using Mac2Driver driver for mac'); + // @ts-ignore Proxy instance is OK + return this.proxydriver.wda.proxy; + } else { + throw new Error( + `proxydriver is unknown type (${typeof this.proxydriver})`, + ); } } diff --git a/test/unit/element.specs.ts b/test/unit/element.specs.ts index 04c7da9..580e297 100644 --- a/test/unit/element.specs.ts +++ b/test/unit/element.specs.ts @@ -2,6 +2,10 @@ import sinon from 'sinon'; import * as utils from '../../src/utils'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; +// @ts-ignore +import { XCUITestDriver } from 'appium-xcuitest-driver'; +// @ts-ignore +import { Mac2Driver } from 'appium-mac2-driver'; import { W3C_ELEMENT_KEY } from 'appium/driver'; import { ELEMENT_CACHE, @@ -137,6 +141,45 @@ describe('Element Interaction Functions', () => { }), ).to.be.true; }); + + it('should use different element body for XCUITestDriver', async () => { + mockAppiumFlutterDriver.proxydriver = new XCUITestDriver(); + + await findElOrEls.call( + mockAppiumFlutterDriver, + 'strategy', + 'selector', + false, + 'context', + ); + + expect( + mockDriver.command.calledWith('/element', 'POST', { + strategy: 'strategy', + selector: 'selector', + context: 'context', + }), + ).to.be.true; + }); + + it('should use different element body for Mac2Driver', async () => { + mockAppiumFlutterDriver.proxydriver = new Mac2Driver(); + + await findElOrEls.call( + mockAppiumFlutterDriver, + 'strategy', + 'selector', + false, + 'context', + ); + expect( + mockDriver.command.calledWith('/element', 'POST', { + using: 'strategy', + value: 'selector', + context: 'context', + }), + ).to.be.true; + }); }); describe('click', () => { From 49495db1d7c5060f9f8e3ef5acb9cce6b618e823 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 10:00:02 +0400 Subject: [PATCH 2/7] update element payload to fix failing tests --- src/commands/element.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commands/element.ts b/src/commands/element.ts index 50c3ff5..a0cc264 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -33,11 +33,12 @@ export async function findElOrEls( ? JSON.parse(selector) : selector; // Special case - // If user is looking for Native IOS/Mac locator - if (!isFlutterLocator && (proxyDriver instanceof XCUITestDriver || proxyDriver instanceof Mac2Driver)) { - return { using: strategy, value: parsedSelector, context }; - } else { + // Use strategy/selector format for Flutter locators and Android driver + if (isFlutterLocator || proxyDriver instanceof AndroidUiautomator2Driver || proxyDriver instanceof XCUITestDriver) { return { strategy, selector: parsedSelector, context }; + } else { + // Use using/value format for all other cases + return { using: strategy, value: parsedSelector, context }; } } From 4d34eade73faec3992398f6e789213f6ede95755 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 10:06:53 +0400 Subject: [PATCH 3/7] Fixed failing unit tests --- src/commands/element.ts | 13 ++++++++----- test/unit/element.specs.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/commands/element.ts b/src/commands/element.ts index a0cc264..b24d4ab 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -33,12 +33,15 @@ export async function findElOrEls( ? JSON.parse(selector) : selector; // Special case - // Use strategy/selector format for Flutter locators and Android driver - if (isFlutterLocator || proxyDriver instanceof AndroidUiautomator2Driver || proxyDriver instanceof XCUITestDriver) { - return { strategy, selector: parsedSelector, context }; - } else { - // Use using/value format for all other cases + // If user is looking for Native IOS/Mac locator + if ( + !isFlutterLocator && + (proxyDriver instanceof XCUITestDriver || + proxyDriver instanceof Mac2Driver) + ) { return { using: strategy, value: parsedSelector, context }; + } else { + return { strategy, selector: parsedSelector, context }; } } diff --git a/test/unit/element.specs.ts b/test/unit/element.specs.ts index 580e297..283e3c5 100644 --- a/test/unit/element.specs.ts +++ b/test/unit/element.specs.ts @@ -72,8 +72,8 @@ describe('Element Interaction Functions', () => { expect(ELEMENT_CACHE.get('elem1')).to.equal(mockDriver); expect( mockDriver.command.calledWith('/element', 'POST', { - using: 'strategy', - value: 'selector', + strategy: 'strategy', + selector: 'selector', context: 'context', }), ).to.be.true; @@ -100,8 +100,8 @@ describe('Element Interaction Functions', () => { expect(ELEMENT_CACHE.get('elem2')).to.equal(mockDriver); expect( mockDriver.command.calledWith('/elements', 'POST', { - using: 'strategy', - value: 'selector', + strategy: 'strategy', + selector: 'selector', context: 'context', }), ).to.be.true; @@ -155,8 +155,8 @@ describe('Element Interaction Functions', () => { expect( mockDriver.command.calledWith('/element', 'POST', { - strategy: 'strategy', - selector: 'selector', + using: 'strategy', + value: 'selector', context: 'context', }), ).to.be.true; @@ -203,8 +203,8 @@ describe('Element Interaction Functions', () => { expect(ELEMENT_CACHE.get('elem1')).to.equal(mockDriver); expect( mockDriver.command.calledWith('/element', 'POST', { - using: 'strategy', - value: 'selector', + strategy: 'strategy', + selector: 'selector', context: 'context', }), ).to.be.true; From d4221865cf11de5f27f12b404a2c677d00981605 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 11:22:41 +0400 Subject: [PATCH 4/7] Workaround for mac click operation when clicked on flutter elements which open native UI --- src/commands/element.ts | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/commands/element.ts b/src/commands/element.ts index b24d4ab..e3a71b1 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -65,9 +65,38 @@ export async function findElOrEls( export async function click(this: AppiumFlutterDriver, element: string) { const driver = ELEMENT_CACHE.get(element); - return await driver.command(`/element/${element}/click`, 'POST', { - element, - }); + + if (this.proxydriver instanceof Mac2Driver) { + this.log.debug('Mac2Driver detected, using non-blocking click'); + + try { + // Working around Mac2Driver issues which is blocking click request when clicking on Flutter elements opens native dialog + // For Flutter elements, we just verify the element is in our cache + if (!ELEMENT_CACHE.has(element)) { + throw new Error('Element not found in cache'); + } + + // Element exists, send click command + driver.command(`/element/${element}/click`, 'POST', { + element, + }).catch((err: Error) => { + // Log error but don't block + this.log.debug(`Click command sent (non-blocking). Any error: ${err.message}`); + }); + + // Return success since element check passed + return true; + } catch (err) { + // Element check failed - this is a legitimate error we should report + this.log.error('Element validation failed before click:', err); + throw new Error(`Element validation failed: ${err.message}`); + } + } else { + // For other drivers, proceed with normal click behavior + return await driver.command(`/element/${element}/click`, 'POST', { + element, + }); + } } export async function getText(this: AppiumFlutterDriver, elementId: string) { From 3db6536877c6e5998334af7a179080f289935637 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 11:40:19 +0400 Subject: [PATCH 5/7] updated code for tests failure --- src/commands/element.ts | 21 ++++++++++++++++----- test/unit/element.specs.ts | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/commands/element.ts b/src/commands/element.ts index e3a71b1..9946d38 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -27,11 +27,22 @@ export async function findElOrEls( const isFlutterLocator = strategy.startsWith('-flutter') || FLUTTER_LOCATORS.includes(strategy); - const parsedSelector = - ['-flutter descendant', '-flutter ancestor'].includes(strategy) && - _.isString(selector) - ? JSON.parse(selector) - : selector; // Special case + let parsedSelector; + if (['-flutter descendant', '-flutter ancestor'].includes(strategy)) { + // Handle descendant/ancestor special case + parsedSelector = _.isString(selector) ? JSON.parse(selector) : selector; + + // For Mac2Driver and XCUITestDriver, format selector differently + if (proxyDriver instanceof XCUITestDriver || proxyDriver instanceof Mac2Driver) { + return { + using: strategy, + value: JSON.stringify(parsedSelector), + context + }; + } + } else { + parsedSelector = selector; + } // If user is looking for Native IOS/Mac locator if ( diff --git a/test/unit/element.specs.ts b/test/unit/element.specs.ts index 283e3c5..72225e3 100644 --- a/test/unit/element.specs.ts +++ b/test/unit/element.specs.ts @@ -70,6 +70,7 @@ describe('Element Interaction Functions', () => { expect(result).to.deep.equal(element); expect(ELEMENT_CACHE.get('elem1')).to.equal(mockDriver); + // Since proxydriver is not Mac2Driver, XCUITestDriver, or AndroidUiautomator2Driver expect( mockDriver.command.calledWith('/element', 'POST', { strategy: 'strategy', From 5ac70b4288bd9174c3129f9d20c369ced0b0e095 Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 15:35:23 +0000 Subject: [PATCH 6/7] reverted dependencies for appium3 --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2df250e..d80d43a 100644 --- a/package.json +++ b/package.json @@ -94,12 +94,12 @@ "appium": "^3.0.0" }, "dependencies": { - "@appium/base-driver": "^9.16.4", - "appium-adb": "^12.4.4", - "appium-ios-device": "^2.7.20", - "appium-uiautomator2-driver": "^4.1.5", - "appium-xcuitest-driver": "9.1.2", - "appium-mac2-driver": "^2.2.2", + "@appium/base-driver": "^10.0.0", + "appium-adb": "^13.0.0", + "appium-ios-device": "^3.0.0", + "appium-uiautomator2-driver": "^5.0.0", + "appium-xcuitest-driver": "^10.0.0", + "appium-mac2-driver": "^3.0.0", "async-retry": "^1.3.3", "asyncbox": "^3.0.0", "bluebird": "^3.7.2", From 208f7ab9ef74d83bf04f02a9d12f88d00cd4e7ab Mon Sep 17 00:00:00 2001 From: Feroz Khan Date: Tue, 23 Sep 2025 19:04:14 +0000 Subject: [PATCH 7/7] fixed formatting --- src/commands/element.ts | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/commands/element.ts b/src/commands/element.ts index 9946d38..2d2a4ba 100644 --- a/src/commands/element.ts +++ b/src/commands/element.ts @@ -30,14 +30,19 @@ export async function findElOrEls( let parsedSelector; if (['-flutter descendant', '-flutter ancestor'].includes(strategy)) { // Handle descendant/ancestor special case - parsedSelector = _.isString(selector) ? JSON.parse(selector) : selector; - + parsedSelector = _.isString(selector) + ? JSON.parse(selector) + : selector; + // For Mac2Driver and XCUITestDriver, format selector differently - if (proxyDriver instanceof XCUITestDriver || proxyDriver instanceof Mac2Driver) { - return { + if ( + proxyDriver instanceof XCUITestDriver || + proxyDriver instanceof Mac2Driver + ) { + return { using: strategy, value: JSON.stringify(parsedSelector), - context + context, }; } } else { @@ -76,10 +81,10 @@ export async function findElOrEls( export async function click(this: AppiumFlutterDriver, element: string) { const driver = ELEMENT_CACHE.get(element); - + if (this.proxydriver instanceof Mac2Driver) { this.log.debug('Mac2Driver detected, using non-blocking click'); - + try { // Working around Mac2Driver issues which is blocking click request when clicking on Flutter elements opens native dialog // For Flutter elements, we just verify the element is in our cache @@ -88,13 +93,17 @@ export async function click(this: AppiumFlutterDriver, element: string) { } // Element exists, send click command - driver.command(`/element/${element}/click`, 'POST', { - element, - }).catch((err: Error) => { - // Log error but don't block - this.log.debug(`Click command sent (non-blocking). Any error: ${err.message}`); - }); - + driver + .command(`/element/${element}/click`, 'POST', { + element, + }) + .catch((err: Error) => { + // Log error but don't block + this.log.debug( + `Click command sent (non-blocking). Any error: ${err.message}`, + ); + }); + // Return success since element check passed return true; } catch (err) {