Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"automationName": "FlutterIntegration",
"platformNames": [
"Android",
"iOS"
"iOS",
"Mac"
],
"mainClass": "AppiumFlutterDriver",
"flutterServerVersion": ">=0.0.18 <1.0.0"
Expand Down Expand Up @@ -98,6 +99,7 @@
"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",
Expand Down
110 changes: 86 additions & 24 deletions src/commands/element.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,28 +19,53 @@ export async function findElOrEls(
): Promise<any> {
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);

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 (
!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) => {
Expand All @@ -52,9 +81,42 @@ 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) {
Expand Down
2 changes: 1 addition & 1 deletion src/desiredCaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const desiredCapConstraints = {
presence: true,
},
platformName: {
inclusionCaseInsensitive: ['iOS', 'Android'],
inclusionCaseInsensitive: ['iOS', 'Android', 'Mac'],
isString: true,
presence: true,
},
Expand Down
7 changes: 6 additions & 1 deletion src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -55,7 +57,7 @@ const WEBVIEW_NO_PROXY = [

export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
// @ts-ignore
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver;
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver | Mac2Driver;
public flutterPort: number | null | undefined;
private internalCaps: DriverCaps<FlutterDriverConstraints> | undefined;
public proxy: JWProxy | undefined;
Expand Down Expand Up @@ -220,6 +222,9 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
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(
Expand Down
30 changes: 30 additions & 0 deletions src/macOS.ts
Original file line number Diff line number Diff line change
@@ -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<Mac2Driver> {
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);
}
1 change: 1 addition & 0 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const PLATFORM = {
IOS: 'ios',
ANDROID: 'android',
MAC: 'mac',
} as const;
8 changes: 8 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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}. ` +
Expand Down
23 changes: 22 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -23,19 +27,36 @@ export const FLUTTER_LOCATORS = [
'text',
'type',
'text containing',
'descendant',
'ancestor',
];
export async function getProxyDriver(
this: AppiumFlutterDriver,
strategy: string,
): Promise<JWProxy | undefined> {
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})`,
);
}
}

Expand Down
56 changes: 50 additions & 6 deletions test/unit/element.specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -66,10 +70,11 @@ 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', {
using: 'strategy',
value: 'selector',
strategy: 'strategy',
selector: 'selector',
context: 'context',
}),
).to.be.true;
Expand All @@ -96,8 +101,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;
Expand Down Expand Up @@ -137,6 +142,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', {
using: 'strategy',
value: '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', () => {
Expand All @@ -160,8 +204,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;
Expand Down
Loading