Skip to content
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
This is an Appium driver for automating macOS applications using Apple's XCTest framework.

> [!IMPORTANT]
> Since major version *3.0.0*, this driver is only compatible with Appium 3.
> Since major version *5.0.0*, this driver is only compatible with Appium 4.


## Documentation
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ and prerequisites of Appium itself can be found in [the Appium documentation](ht

| Mac2 driver version | Supported Appium server version |
| --- | --- |
| >= 3.0.0 | Appium 3 |
| >= 5.0.0 | Appium 4 |
| 3.0.0 - 4.6.1 | Appium 3 |
| 1.0.0 - 2.2.2 | Appium 2 |

## Installation
Expand Down
9 changes: 9 additions & 0 deletions lib/commands/app-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import type {StringRecord} from '@appium/types';

import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosLaunchApp(...args: Parameters<typeof macosLaunchApp>): ReturnType<typeof macosLaunchApp>;
macosActivateApp(...args: Parameters<typeof macosActivateApp>): ReturnType<typeof macosActivateApp>;
macosTerminateApp(...args: Parameters<typeof macosTerminateApp>): ReturnType<typeof macosTerminateApp>;
macosQueryAppState(...args: Parameters<typeof macosQueryAppState>): ReturnType<typeof macosQueryAppState>;
}
}

/**
* Start an app with given bundle identifier or activates it
* if the app is already running. An exception is thrown if the
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/applescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import {exec} from 'teen_process';

import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosExecAppleScript(...args: Parameters<typeof macosExecAppleScript>): ReturnType<typeof macosExecAppleScript>;
}
}

const OSASCRIPT = 'osascript';
const APPLE_SCRIPT_FEATURE = 'apple_script';

Expand Down
8 changes: 8 additions & 0 deletions lib/commands/audit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosPerformAccessibilityAudit(
...args: Parameters<typeof macosPerformAccessibilityAudit>
): ReturnType<typeof macosPerformAccessibilityAudit>;
}
}

export interface AccessibilityAuditItem {
detailedDescription: string;
compactDescription: string;
Expand Down
7 changes: 7 additions & 0 deletions lib/commands/clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosGetClipboard(...args: Parameters<typeof macosGetClipboard>): ReturnType<typeof macosGetClipboard>;
macosSetClipboard(...args: Parameters<typeof macosSetClipboard>): ReturnType<typeof macosSetClipboard>;
}
}

/**
* Sets the content of the clipboard.
*
Expand Down
12 changes: 11 additions & 1 deletion lib/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import axios from 'axios';
import {findAPortNotInUse} from 'portscanner';

import type {Mac2Driver} from '../driver.js';
import log from '../logger.js';
import {log} from '../logger.js';
import type {CdpPage, CdpVersionInfo, WebViewDetails} from '../types.js';
import {getModuleRoot} from '../utils.js';

declare module '../driver.js' {
interface Mac2Driver {
getCurrentContext(...args: Parameters<typeof getCurrentContext>): ReturnType<typeof getCurrentContext>;
getContexts(...args: Parameters<typeof getContexts>): ReturnType<typeof getContexts>;
setContext(...args: Parameters<typeof setContext>): ReturnType<typeof setContext>;
getWebViewDetails(...args: Parameters<typeof getWebViewDetails>): ReturnType<typeof getWebViewDetails>;
macosGetContexts(...args: Parameters<typeof macosGetContexts>): ReturnType<typeof macosGetContexts>;
}
}

export const NATIVE_APP = 'NATIVE_APP';
export const WEBVIEW_PREFIX = 'WEBVIEW_';
export const DEBUGGING_PORT_FLAG = '--remote-debugging-port';
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type {StringRecord} from '@appium/types';

import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
execute(...args: Parameters<typeof execute>): ReturnType<typeof execute>;
}
}

const EXECUTE_SCRIPT_PREFIX = 'macos:';

/**
Expand Down
57 changes: 53 additions & 4 deletions lib/commands/find.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import {errors, isErrorType} from 'appium/driver.js';
import {util} from 'appium/support.js';
import {ConditionUnmetError} from 'asyncbox';

import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
findElOrEls(...args: Parameters<typeof findElOrEls>): ReturnType<typeof findElOrEls>;
}
}

// Older asyncbox (possibly bundled with the base driver) throws a plain Error
function isConditionUnmet(e: unknown): boolean {
return e instanceof ConditionUnmetError || /Condition unmet/.test((e as Error)?.message ?? '');
}

/**
* This is needed to make lookup by image working
*
Expand Down Expand Up @@ -32,8 +45,44 @@ export async function findElOrEls(
normalizedStrategy = 'class chain';
}

return await this.wda.proxy.command(endpoint, 'POST', {
using: normalizedStrategy,
value: selector,
});
const doFind = async (): Promise<any> =>
await this.wda.proxy.command(endpoint, 'POST', {
using: normalizedStrategy,
value: selector,
});
if (!this.implicitWaitMs) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use this.implicitWaitMs === 0 here? Appium 4 accepts implicit: null to disable the timeout, and its implicitWaitForCondition maps that value to Infinity. This falsy check instead bypasses the helper, so a lookup fails immediately on the first NoSuchElementError even if the element appears on the next attempt. I reproduced this with implicit: null and a stub that fails once and then succeeds: only one lookup is made. XCUITest's native lookup delegates to the helper for an active session, so it preserves the unlimited wait. Checking explicitly for zero would retain the default behavior agreed in #436 while honoring the new nullable timeout semantics. A regression test for null would cover this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The check is now this.implicitWaitMs === 0, so null goes through implicitWaitForCondition and waits without a deadline. Added a regression test (fail once, then succeed with implicit: null).

return await doFind();
}

// The WDA server ignores the /timeouts API, so the implicit wait is applied here
let result: any;
let lastError: Error | undefined;
// A lookup error other than NoSuchElementError stops the wait and must reach the caller as is
let lookupError: unknown;
try {
await this.implicitWaitForCondition(async () => {
try {
result = await doFind();
} catch (e) {
if (!isErrorType(e, errors.NoSuchElementError)) {
lookupError = e;
throw e;
}
lastError = e as Error;
return false;
}
return !mult || (Array.isArray(result) && result.length > 0);
});
} catch (e) {
if (lookupError !== undefined) {
throw lookupError;
}
if (!isConditionUnmet(e)) {
throw e;
}
if (!mult && lastError) {
throw lastError;
}
}
return result;
}
24 changes: 24 additions & 0 deletions lib/commands/gestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ import {util} from 'appium/support.js';
import type {Mac2Driver} from '../driver.js';
import type {KeyOptions} from '../types.js';

declare module '../driver.js' {
interface Mac2Driver {
macosSetValue(...args: Parameters<typeof macosSetValue>): ReturnType<typeof macosSetValue>;
macosClick(...args: Parameters<typeof macosClick>): ReturnType<typeof macosClick>;
macosScroll(...args: Parameters<typeof macosScroll>): ReturnType<typeof macosScroll>;
macosSwipe(...args: Parameters<typeof macosSwipe>): ReturnType<typeof macosSwipe>;
macosRightClick(...args: Parameters<typeof macosRightClick>): ReturnType<typeof macosRightClick>;
macosHover(...args: Parameters<typeof macosHover>): ReturnType<typeof macosHover>;
macosDoubleClick(...args: Parameters<typeof macosDoubleClick>): ReturnType<typeof macosDoubleClick>;
macosClickAndDrag(...args: Parameters<typeof macosClickAndDrag>): ReturnType<typeof macosClickAndDrag>;
macosClickAndDragAndHold(
...args: Parameters<typeof macosClickAndDragAndHold>
): ReturnType<typeof macosClickAndDragAndHold>;
macosKeys(...args: Parameters<typeof macosKeys>): ReturnType<typeof macosKeys>;
macosPressAndHold(...args: Parameters<typeof macosPressAndHold>): ReturnType<typeof macosPressAndHold>;
macosTap(...args: Parameters<typeof macosTap>): ReturnType<typeof macosTap>;
macosDoubleTap(...args: Parameters<typeof macosDoubleTap>): ReturnType<typeof macosDoubleTap>;
macosPressAndDrag(...args: Parameters<typeof macosPressAndDrag>): ReturnType<typeof macosPressAndDrag>;
macosPressAndDragAndHold(
...args: Parameters<typeof macosPressAndDragAndHold>
): ReturnType<typeof macosPressAndDragAndHold>;
}
}

/**
* Set value to the given element.
* Note:
Expand Down
15 changes: 15 additions & 0 deletions lib/commands/native-record-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import {BIDI_EVENT_NAME} from './bidi/constants.js';
import {toNativeVideoChunkAddedEvent} from './bidi/models.js';
import {uploadRecordedMedia} from './helpers.js';

declare module '../driver.js' {
interface Mac2Driver {
macosStartNativeScreenRecording(
...args: Parameters<typeof macosStartNativeScreenRecording>
): ReturnType<typeof macosStartNativeScreenRecording>;
macosGetNativeScreenRecordingInfo(
...args: Parameters<typeof macosGetNativeScreenRecordingInfo>
): ReturnType<typeof macosGetNativeScreenRecordingInfo>;
macosStopNativeScreenRecording(
...args: Parameters<typeof macosStopNativeScreenRecording>
): ReturnType<typeof macosStopNativeScreenRecording>;
macosListDisplays(...args: Parameters<typeof macosListDisplays>): ReturnType<typeof macosListDisplays>;
}
}

const RECORDING_STARTUP_TIMEOUT_MS = 5000;
const BUFFER_SIZE = 0xffff;
const MONITORING_INTERVAL_DURATION_MS = 1000;
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosDeepLink(...args: Parameters<typeof macosDeepLink>): ReturnType<typeof macosDeepLink>;
}
}

/**
* Opens the given URL with the default or the given application.
* Xcode must be at version 14.3+.
Expand Down
13 changes: 13 additions & 0 deletions lib/commands/record-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import {SubProcess} from 'teen_process';
import type {Mac2Driver} from '../driver.js';
import {uploadRecordedMedia} from './helpers.js';

declare module '../driver.js' {
interface Mac2Driver {
startRecordingScreen(...args: Parameters<typeof startRecordingScreen>): ReturnType<typeof startRecordingScreen>;
stopRecordingScreen(...args: Parameters<typeof stopRecordingScreen>): ReturnType<typeof stopRecordingScreen>;
macosStartRecordingScreen(
...args: Parameters<typeof macosStartRecordingScreen>
): ReturnType<typeof macosStartRecordingScreen>;
macosStopRecordingScreen(
...args: Parameters<typeof macosStopRecordingScreen>
): ReturnType<typeof macosStopRecordingScreen>;
}
}

const RETRY_PAUSE = 300;
const RETRY_TIMEOUT = 5000;
const DEFAULT_TIME_LIMIT = 60 * 10; // 10 minutes
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/screenshots.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type {Mac2Driver} from '../driver.js';
import type {ScreenshotsInfo} from '../types.js';

declare module '../driver.js' {
interface Mac2Driver {
macosScreenshots(...args: Parameters<typeof macosScreenshots>): ReturnType<typeof macosScreenshots>;
}
}

/**
* Retrieves screenshots of each display available to macOS
*
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type {Mac2Driver} from '../driver.js';

declare module '../driver.js' {
interface Mac2Driver {
macosSource(...args: Parameters<typeof macosSource>): ReturnType<typeof macosSource>;
}
}

/**
* Retrieves the string representation of the current application
*
Expand Down
7 changes: 7 additions & 0 deletions lib/commands/windows.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type {Mac2Driver} from '../driver.js';
import type {AdjustedWindowInfo, WindowInfo, WindowState} from '../types.js';

declare module '../driver.js' {
interface Mac2Driver {
macosListWindows(...args: Parameters<typeof macosListWindows>): ReturnType<typeof macosListWindows>;
macosAdjustWindow(...args: Parameters<typeof macosAdjustWindow>): ReturnType<typeof macosAdjustWindow>;
}
}

/**
* Lists the windows of the current application
*
Expand Down
4 changes: 1 addition & 3 deletions lib/constraints.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {Constraints} from '@appium/types';

const MAC2_CONSTRAINTS = {
export const MAC2_CONSTRAINTS = {
systemPort: {
isNumber: true,
},
Expand Down Expand Up @@ -66,6 +66,4 @@ const MAC2_CONSTRAINTS = {
},
} as const satisfies Constraints;

export default MAC2_CONSTRAINTS;

export type Mac2Constraints = typeof MAC2_CONSTRAINTS;
Loading
Loading