Skip to content

Commit 20e47ac

Browse files
ferozkFeroz Khan
andauthored
ci: Added support for macOS (#143)
* feat: Added support for macOS * update element payload to fix failing tests * Fixed failing unit tests * Workaround for mac click operation when clicked on flutter elements which open native UI * updated code for tests failure * reverted dependencies for appium3 * fixed formatting --------- Co-authored-by: Feroz Khan <[email protected]>
1 parent 6790281 commit 20e47ac

File tree

10 files changed

+212
-34
lines changed

10 files changed

+212
-34
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ Get the latest version from `https://pub.dev/packages/appium_flutter_server/inst
9696
flutter build ipa --release integration_test/appium_test.dart
9797
```
9898
99+
7. Build the MacOS app:
100+
```bash
101+
flutter build macos --release integration_test/appium_test.dart
102+
```
103+
99104
Bingo! You are ready to run your tests using Appium Flutter Integration Driver.
100105
101106
Check if your Flutter app is running on the device or emulator.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"automationName": "FlutterIntegration",
3030
"platformNames": [
3131
"Android",
32-
"iOS"
32+
"iOS",
33+
"Mac"
3334
],
3435
"mainClass": "AppiumFlutterDriver",
3536
"flutterServerVersion": ">=0.0.18 <1.0.0"
@@ -98,6 +99,7 @@
9899
"appium-ios-device": "^3.0.0",
99100
"appium-uiautomator2-driver": "^5.0.0",
100101
"appium-xcuitest-driver": "^10.0.0",
102+
"appium-mac2-driver": "^3.0.0",
101103
"async-retry": "^1.3.3",
102104
"asyncbox": "^3.0.0",
103105
"bluebird": "^3.7.2",

src/commands/element.ts

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import _ from 'lodash';
2-
import { getProxyDriver } from '../utils';
2+
import { getProxyDriver, FLUTTER_LOCATORS } from '../utils';
33
import { JWProxy } from 'appium/driver';
44
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
5+
// @ts-ignore
6+
import { XCUITestDriver } from 'appium-xcuitest-driver';
7+
// @ts-ignore
8+
import { Mac2Driver } from 'appium-mac2-driver';
59
import { W3C_ELEMENT_KEY } from 'appium/driver';
610
import type { AppiumFlutterDriver } from '../driver';
711

@@ -15,28 +19,53 @@ export async function findElOrEls(
1519
): Promise<any> {
1620
const driver = await getProxyDriver.bind(this)(strategy);
1721
let elementBody;
18-
if (
19-
!(driver instanceof JWProxy) &&
20-
!(this.proxydriver instanceof AndroidUiautomator2Driver)
22+
function constructFindElementPayload(
23+
strategy: string,
24+
selector: string,
25+
proxyDriver: XCUITestDriver | AndroidUiautomator2Driver | Mac2Driver,
2126
) {
22-
elementBody = {
23-
using: strategy,
24-
value: selector,
25-
context, //this needs be validated
26-
};
27-
} else {
28-
elementBody = {
29-
strategy,
30-
selector: ['-flutter descendant', '-flutter ancestor'].includes(
31-
strategy,
32-
)
33-
? _.isString(selector)
34-
? JSON.parse(selector)
35-
: selector
36-
: selector,
37-
context,
38-
};
27+
const isFlutterLocator =
28+
strategy.startsWith('-flutter') || FLUTTER_LOCATORS.includes(strategy);
29+
30+
let parsedSelector;
31+
if (['-flutter descendant', '-flutter ancestor'].includes(strategy)) {
32+
// Handle descendant/ancestor special case
33+
parsedSelector = _.isString(selector)
34+
? JSON.parse(selector)
35+
: selector;
36+
37+
// For Mac2Driver and XCUITestDriver, format selector differently
38+
if (
39+
proxyDriver instanceof XCUITestDriver ||
40+
proxyDriver instanceof Mac2Driver
41+
) {
42+
return {
43+
using: strategy,
44+
value: JSON.stringify(parsedSelector),
45+
context,
46+
};
47+
}
48+
} else {
49+
parsedSelector = selector;
50+
}
51+
52+
// If user is looking for Native IOS/Mac locator
53+
if (
54+
!isFlutterLocator &&
55+
(proxyDriver instanceof XCUITestDriver ||
56+
proxyDriver instanceof Mac2Driver)
57+
) {
58+
return { using: strategy, value: parsedSelector, context };
59+
} else {
60+
return { strategy, selector: parsedSelector, context };
61+
}
3962
}
63+
64+
elementBody = constructFindElementPayload(
65+
strategy,
66+
selector,
67+
this.proxydriver,
68+
);
4069
if (mult) {
4170
const response = await driver.command('/elements', 'POST', elementBody);
4271
response.forEach((element: any) => {
@@ -52,9 +81,42 @@ export async function findElOrEls(
5281

5382
export async function click(this: AppiumFlutterDriver, element: string) {
5483
const driver = ELEMENT_CACHE.get(element);
55-
return await driver.command(`/element/${element}/click`, 'POST', {
56-
element,
57-
});
84+
85+
if (this.proxydriver instanceof Mac2Driver) {
86+
this.log.debug('Mac2Driver detected, using non-blocking click');
87+
88+
try {
89+
// Working around Mac2Driver issues which is blocking click request when clicking on Flutter elements opens native dialog
90+
// For Flutter elements, we just verify the element is in our cache
91+
if (!ELEMENT_CACHE.has(element)) {
92+
throw new Error('Element not found in cache');
93+
}
94+
95+
// Element exists, send click command
96+
driver
97+
.command(`/element/${element}/click`, 'POST', {
98+
element,
99+
})
100+
.catch((err: Error) => {
101+
// Log error but don't block
102+
this.log.debug(
103+
`Click command sent (non-blocking). Any error: ${err.message}`,
104+
);
105+
});
106+
107+
// Return success since element check passed
108+
return true;
109+
} catch (err) {
110+
// Element check failed - this is a legitimate error we should report
111+
this.log.error('Element validation failed before click:', err);
112+
throw new Error(`Element validation failed: ${err.message}`);
113+
}
114+
} else {
115+
// For other drivers, proceed with normal click behavior
116+
return await driver.command(`/element/${element}/click`, 'POST', {
117+
element,
118+
});
119+
}
58120
}
59121

60122
export async function getText(this: AppiumFlutterDriver, elementId: string) {

src/desiredCaps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const desiredCapConstraints = {
77
presence: true,
88
},
99
platformName: {
10-
inclusionCaseInsensitive: ['iOS', 'Android'],
10+
inclusionCaseInsensitive: ['iOS', 'Android', 'Mac'],
1111
isString: true,
1212
presence: true,
1313
},

src/driver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type FlutterDriverConstraints = typeof desiredCapConstraints;
1010
// @ts-ignore
1111
import { XCUITestDriver } from 'appium-xcuitest-driver';
1212
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
13+
// @ts-ignore
14+
import { Mac2Driver } from 'appium-mac2-driver';
1315
import { createSession as createSessionMixin } from './session';
1416
import {
1517
findElOrEls,
@@ -55,7 +57,7 @@ const WEBVIEW_NO_PROXY = [
5557

5658
export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
5759
// @ts-ignore
58-
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver;
60+
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver | Mac2Driver;
5961
public flutterPort: number | null | undefined;
6062
private internalCaps: DriverCaps<FlutterDriverConstraints> | undefined;
6163
public proxy: JWProxy | undefined;
@@ -220,6 +222,9 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
220222
this.currentContext === this.NATIVE_CONTEXT_NAME &&
221223
isFlutterDriverCommand(command)
222224
) {
225+
this.log.debug(
226+
`executeCommand: command ${command} is flutter command using flutter driver`,
227+
);
223228
return await super.executeCommand(command, ...args);
224229
} else {
225230
this.log.info(

src/macOS.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { AppiumFlutterDriver } from './driver';
2+
// @ts-ignore
3+
import { Mac2Driver } from 'appium-mac2-driver';
4+
import type { InitialOpts } from '@appium/types';
5+
import { DEVICE_CONNECTIONS_FACTORY } from './iProxy';
6+
7+
export async function startMacOsSession(
8+
this: AppiumFlutterDriver,
9+
...args: any[]
10+
): Promise<Mac2Driver> {
11+
this.log.info(`Starting an MacOs proxy session`);
12+
const macOsDriver = new Mac2Driver({} as InitialOpts);
13+
await macOsDriver.createSession(...args);
14+
return macOsDriver;
15+
}
16+
17+
export async function macOsPortForward(
18+
udid: string,
19+
systemPort: number,
20+
devicePort: number,
21+
) {
22+
await DEVICE_CONNECTIONS_FACTORY.requestConnection(udid, systemPort, {
23+
usePortForwarding: true,
24+
devicePort: devicePort,
25+
});
26+
}
27+
28+
export function macOsRemovePortForward(udid: string, systemPort: number) {
29+
DEVICE_CONNECTIONS_FACTORY.releaseConnection(udid, systemPort);
30+
}

src/platform.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const PLATFORM = {
22
IOS: 'ios',
33
ANDROID: 'android',
4+
MAC: 'mac',
45
} as const;

src/session.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import _ from 'lodash';
33
import { PLATFORM } from './platform';
44
import { startAndroidSession } from './android';
55
import { startIOSSession } from './iOS';
6+
import { startMacOsSession } from './macOS';
67
import type { DefaultCreateSessionResult } from '@appium/types';
78

89
export async function createSession(
@@ -28,6 +29,13 @@ export async function createSession(
2829
this.proxydriver.denyInsecure = this.denyInsecure;
2930
this.proxydriver.allowInsecure = this.allowInsecure;
3031
break;
32+
case PLATFORM.MAC:
33+
this.proxydriver = await startMacOsSession.bind(this)(...args);
34+
this.proxydriver.relaxedSecurityEnabled =
35+
this.relaxedSecurityEnabled;
36+
this.proxydriver.denyInsecure = this.denyInsecure;
37+
this.proxydriver.allowInsecure = this.allowInsecure;
38+
break;
3139
default:
3240
this.log.errorWithException(
3341
`Unsupported platformName: ${caps.platformName}. ` +

src/utils.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
2+
// @ts-ignore
3+
import { XCUITestDriver } from 'appium-xcuitest-driver';
4+
// @ts-ignore
5+
import { Mac2Driver } from 'appium-mac2-driver';
26
import { findAPortNotInUse } from 'portscanner';
37
import { waitForCondition } from 'asyncbox';
48
import { JWProxy } from '@appium/base-driver';
@@ -23,19 +27,36 @@ export const FLUTTER_LOCATORS = [
2327
'text',
2428
'type',
2529
'text containing',
30+
'descendant',
31+
'ancestor',
2632
];
2733
export async function getProxyDriver(
2834
this: AppiumFlutterDriver,
2935
strategy: string,
3036
): Promise<JWProxy | undefined> {
3137
if (strategy.startsWith('-flutter') || FLUTTER_LOCATORS.includes(strategy)) {
38+
this.log.debug(
39+
`getProxyDriver: using flutter driver, strategy: ${strategy}`,
40+
);
3241
return this.proxy;
3342
} else if (this.proxydriver instanceof AndroidUiautomator2Driver) {
43+
this.log.debug(
44+
'getProxyDriver: using AndroidUiautomator2Driver driver for Android',
45+
);
3446
// @ts-ignore Proxy instance is OK
3547
return this.proxydriver.uiautomator2.jwproxy;
36-
} else {
48+
} else if (this.proxydriver instanceof XCUITestDriver) {
49+
this.log.debug('getProxyDriver: using XCUITestDriver driver for iOS');
3750
// @ts-ignore Proxy instance is OK
3851
return this.proxydriver.wda.jwproxy;
52+
} else if (this.proxydriver instanceof Mac2Driver) {
53+
this.log.debug('getProxyDriver: using Mac2Driver driver for mac');
54+
// @ts-ignore Proxy instance is OK
55+
return this.proxydriver.wda.proxy;
56+
} else {
57+
throw new Error(
58+
`proxydriver is unknown type (${typeof this.proxydriver})`,
59+
);
3960
}
4061
}
4162

test/unit/element.specs.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
import sinon from 'sinon';
33
import * as utils from '../../src/utils';
44
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
5+
// @ts-ignore
6+
import { XCUITestDriver } from 'appium-xcuitest-driver';
7+
// @ts-ignore
8+
import { Mac2Driver } from 'appium-mac2-driver';
59
import { W3C_ELEMENT_KEY } from 'appium/driver';
610
import {
711
ELEMENT_CACHE,
@@ -66,10 +70,11 @@ describe('Element Interaction Functions', () => {
6670

6771
expect(result).to.deep.equal(element);
6872
expect(ELEMENT_CACHE.get('elem1')).to.equal(mockDriver);
73+
// Since proxydriver is not Mac2Driver, XCUITestDriver, or AndroidUiautomator2Driver
6974
expect(
7075
mockDriver.command.calledWith('/element', 'POST', {
71-
using: 'strategy',
72-
value: 'selector',
76+
strategy: 'strategy',
77+
selector: 'selector',
7378
context: 'context',
7479
}),
7580
).to.be.true;
@@ -96,8 +101,8 @@ describe('Element Interaction Functions', () => {
96101
expect(ELEMENT_CACHE.get('elem2')).to.equal(mockDriver);
97102
expect(
98103
mockDriver.command.calledWith('/elements', 'POST', {
99-
using: 'strategy',
100-
value: 'selector',
104+
strategy: 'strategy',
105+
selector: 'selector',
101106
context: 'context',
102107
}),
103108
).to.be.true;
@@ -137,6 +142,45 @@ describe('Element Interaction Functions', () => {
137142
}),
138143
).to.be.true;
139144
});
145+
146+
it('should use different element body for XCUITestDriver', async () => {
147+
mockAppiumFlutterDriver.proxydriver = new XCUITestDriver();
148+
149+
await findElOrEls.call(
150+
mockAppiumFlutterDriver,
151+
'strategy',
152+
'selector',
153+
false,
154+
'context',
155+
);
156+
157+
expect(
158+
mockDriver.command.calledWith('/element', 'POST', {
159+
using: 'strategy',
160+
value: 'selector',
161+
context: 'context',
162+
}),
163+
).to.be.true;
164+
});
165+
166+
it('should use different element body for Mac2Driver', async () => {
167+
mockAppiumFlutterDriver.proxydriver = new Mac2Driver();
168+
169+
await findElOrEls.call(
170+
mockAppiumFlutterDriver,
171+
'strategy',
172+
'selector',
173+
false,
174+
'context',
175+
);
176+
expect(
177+
mockDriver.command.calledWith('/element', 'POST', {
178+
using: 'strategy',
179+
value: 'selector',
180+
context: 'context',
181+
}),
182+
).to.be.true;
183+
});
140184
});
141185

142186
describe('click', () => {
@@ -160,8 +204,8 @@ describe('Element Interaction Functions', () => {
160204
expect(ELEMENT_CACHE.get('elem1')).to.equal(mockDriver);
161205
expect(
162206
mockDriver.command.calledWith('/element', 'POST', {
163-
using: 'strategy',
164-
value: 'selector',
207+
strategy: 'strategy',
208+
selector: 'selector',
165209
context: 'context',
166210
}),
167211
).to.be.true;

0 commit comments

Comments
 (0)