Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
14 changes: 8 additions & 6 deletions 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 @@ -93,11 +94,12 @@
"appium": "^3.0.0"
},
"dependencies": {
"@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/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",
"async-retry": "^1.3.3",
"asyncbox": "^3.0.0",
"bluebird": "^3.7.2",
Expand Down
101 changes: 77 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,48 @@ 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 +76,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) {
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