appium-mac2-driver 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/WebDriverAgentMac/IntegrationTests/AMWindowTests.m +427 -0
  3. package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMWindowCommands.m +63 -0
  4. package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +2 -0
  5. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.h +3 -0
  6. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.m +13 -0
  7. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +4 -0
  8. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +23 -0
  9. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +18 -0
  10. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +43 -28
  11. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.h +111 -0
  12. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.m +780 -0
  13. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.h +61 -0
  14. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.m +108 -0
  15. package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +20 -0
  16. package/build/lib/commands/native-record-screen.d.ts +2 -0
  17. package/build/lib/commands/native-record-screen.d.ts.map +1 -1
  18. package/build/lib/commands/native-record-screen.js.map +1 -1
  19. package/build/lib/commands/windows.d.ts +22 -0
  20. package/build/lib/commands/windows.d.ts.map +1 -0
  21. package/build/lib/commands/windows.js +32 -0
  22. package/build/lib/commands/windows.js.map +1 -0
  23. package/build/lib/driver.d.ts +12 -0
  24. package/build/lib/driver.d.ts.map +1 -1
  25. package/build/lib/driver.js +3 -0
  26. package/build/lib/driver.js.map +1 -1
  27. package/build/lib/execute-method-map.d.ts +9 -0
  28. package/build/lib/execute-method-map.d.ts.map +1 -1
  29. package/build/lib/execute-method-map.js +9 -0
  30. package/build/lib/execute-method-map.js.map +1 -1
  31. package/build/lib/types.d.ts +26 -0
  32. package/build/lib/types.d.ts.map +1 -1
  33. package/lib/commands/native-record-screen.ts +2 -0
  34. package/lib/commands/windows.ts +44 -0
  35. package/lib/driver.ts +4 -0
  36. package/lib/execute-method-map.ts +9 -0
  37. package/lib/types.ts +28 -0
  38. package/node_modules/bare-url/README.md +4 -0
  39. package/node_modules/bare-url/index.d.ts +3 -0
  40. package/node_modules/bare-url/index.js +42 -18
  41. package/node_modules/bare-url/lib/errors.js +4 -0
  42. package/node_modules/bare-url/package.json +1 -1
  43. package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
  44. package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
  45. package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
  46. package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
  47. package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
  48. package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
  49. package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
  50. package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
  51. package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
  52. package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
  53. package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
  54. package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
  55. package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
  56. package/package.json +3 -2
  57. package/scripts/setup-accessibility.mjs +175 -0
@@ -0,0 +1,44 @@
1
+ import type {Mac2Driver} from '../driver.js';
2
+ import type {AdjustedWindowInfo, WindowInfo, WindowState} from '../types.js';
3
+
4
+ /**
5
+ * Lists the windows of the current application
6
+ *
7
+ * @returns Information about each window, including its element identifier
8
+ */
9
+ export async function macosListWindows(this: Mac2Driver): Promise<WindowInfo[]> {
10
+ return (await this.wda.proxy.command('/wda/windows', 'GET')) as WindowInfo[];
11
+ }
12
+
13
+ /**
14
+ * Changes the state and/or geometry of an application window, optionally moving it to another display
15
+ *
16
+ * @param windowId - Identifier of the window element. The main window is used by default
17
+ * @param state - The state to put the window into
18
+ * @param x - Horizontal origin. Global, or relative to the display if `displayId` is set
19
+ * @param y - Vertical origin. Global, or relative to the display if `displayId` is set
20
+ * @param width - Window width
21
+ * @param height - Window height
22
+ * @param displayId - Identifier of the display to move the window to
23
+ * @returns The updated window information. `windowId` is null if a minimized window was adjusted
24
+ */
25
+ export async function macosAdjustWindow(
26
+ this: Mac2Driver,
27
+ windowId?: string,
28
+ state?: WindowState,
29
+ x?: number,
30
+ y?: number,
31
+ width?: number,
32
+ height?: number,
33
+ displayId?: number,
34
+ ): Promise<AdjustedWindowInfo> {
35
+ return (await this.wda.proxy.command('/wda/window/adjust', 'POST', {
36
+ windowId,
37
+ state,
38
+ x,
39
+ y,
40
+ width,
41
+ height,
42
+ displayId,
43
+ })) as AdjustedWindowInfo;
44
+ }
package/lib/driver.ts CHANGED
@@ -25,6 +25,7 @@ import * as navigationCommands from './commands/navigation.js';
25
25
  import * as recordScreenCommands from './commands/record-screen.js';
26
26
  import * as screenshotCommands from './commands/screenshots.js';
27
27
  import * as sourceCommands from './commands/source.js';
28
+ import * as windowCommands from './commands/windows.js';
28
29
  import MAC2_CONSTRAINTS, {type Mac2Constraints} from './constraints.js';
29
30
  import {executeMethodMap} from './execute-method-map.js';
30
31
  import log from './logger.js';
@@ -111,6 +112,9 @@ export class Mac2Driver
111
112
 
112
113
  macosSource = sourceCommands.macosSource;
113
114
 
115
+ macosListWindows = windowCommands.macosListWindows;
116
+ macosAdjustWindow = windowCommands.macosAdjustWindow;
117
+
114
118
  _videoChunksBroadcaster!: nativeScreenRecordingCommands.NativeVideoChunksBroadcaster;
115
119
  _screenRecorder: recordScreenCommands.ScreenRecorder | null = null;
116
120
  public proxyReqRes!: (...args: any) => any;
@@ -130,6 +130,15 @@ export const executeMethodMap = {
130
130
  optional: ['displayId'],
131
131
  },
132
132
  },
133
+ 'macos: listWindows': {
134
+ command: 'macosListWindows',
135
+ },
136
+ 'macos: adjustWindow': {
137
+ command: 'macosAdjustWindow',
138
+ params: {
139
+ optional: ['windowId', 'state', 'x', 'y', 'width', 'height', 'displayId'],
140
+ },
141
+ },
133
142
  'macos: appleScript': {
134
143
  command: 'macosExecAppleScript',
135
144
  params: {
package/lib/types.ts CHANGED
@@ -26,3 +26,31 @@ export interface ScreenshotInfo {
26
26
 
27
27
  /** A dictionary where each key contains a unique display identifier */
28
28
  export type ScreenshotsInfo = StringRecord<ScreenshotInfo>;
29
+
30
+ export interface Rect {
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ }
36
+
37
+ export type WindowState = 'normal' | 'minimized' | 'maximized' | 'fullscreen';
38
+
39
+ export interface WindowInfo {
40
+ /** Identifier of the window element */
41
+ windowId: string;
42
+ title: string;
43
+ rect: Rect;
44
+ /** The display that contains most of the window, if any */
45
+ displayId: number | null;
46
+ /** Traits below are null if the Accessibility permission is not granted */
47
+ isMain: boolean | null;
48
+ isFocused: boolean | null;
49
+ isMinimized: boolean | null;
50
+ isFullscreen: boolean | null;
51
+ isMovable: boolean | null;
52
+ isResizable: boolean | null;
53
+ }
54
+
55
+ /** `windowId` is null if a minimized window has been adjusted */
56
+ export type AdjustedWindowInfo = Omit<WindowInfo, 'windowId'> & {windowId: string | null};
@@ -28,6 +28,10 @@ require('bare-url/global')
28
28
 
29
29
  See the [`bare-url` reference](https://docs.pears.com/reference/bare/modules/bare-url).
30
30
 
31
+ ## Threat model
32
+
33
+ `bare-url` is one of the addons Bare compiles into its binary, so it inherits [Bare's threat model](https://github.com/holepunchto/bare/blob/main/docs/threat-model.md). See [`docs/threat-model.md`](docs/threat-model.md) for where this addon sits in it.
34
+
31
35
  ## License
32
36
 
33
37
  Apache-2.0
@@ -87,6 +87,9 @@ declare namespace URL {
87
87
  /**
88
88
  * Convert a platform-specific file path to a `file:` URL.
89
89
  * @param pathname - The platform-specific file path to convert.
90
+ * @throws {INVALID_ARG_VALUE} (Windows) the path is a UNC path with an empty server name or
91
+ * no resource path.
92
+ * @throws {INVALID_URL} the path does not form a valid `file:` URL.
90
93
  */
91
94
  export function pathToFileURL(pathname: string): URL
92
95
 
@@ -494,6 +494,26 @@ function encodePath(value) {
494
494
  return value.replace(pathDelimiterAll, (match) => escapes[match])
495
495
  }
496
496
 
497
+ function encodeFilePath(value) {
498
+ // Paths hardly ever contain any of these, so one pass to rule them out is
499
+ // cheaper than the six or seven replacements it stands in for.
500
+ if (!reserved.test(value)) return value
501
+
502
+ value = value
503
+ .replaceAll('%', '%25') // Must be first
504
+ .replaceAll('#', '%23')
505
+ .replaceAll('?', '%3f')
506
+ .replaceAll('\n', '%0a')
507
+ .replaceAll('\r', '%0d')
508
+ .replaceAll('\t', '%09')
509
+
510
+ if (!isWindows) {
511
+ value = value.replaceAll('\\', '%5c')
512
+ }
513
+
514
+ return value
515
+ }
516
+
497
517
  function encodeQuery(value) {
498
518
  if (value.indexOf('#') === -1) return value
499
519
 
@@ -600,7 +620,27 @@ exports.fileURLToPath = function fileURLToPath(url) {
600
620
  }
601
621
 
602
622
  exports.pathToFileURL = function pathToFileURL(pathname) {
603
- let resolved = path.resolve(pathname)
623
+ const isUNC = isWindows && pathname.startsWith('\\\\')
624
+
625
+ let resolved = isUNC ? pathname : path.resolve(pathname)
626
+
627
+ if (isUNC || (isWindows && resolved.startsWith('\\\\'))) {
628
+ // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
629
+ const hostStart = resolved.startsWith('\\\\?\\UNC\\') ? 8 : 2
630
+ const hostEnd = resolved.indexOf('\\', hostStart)
631
+
632
+ if (hostEnd === -1) {
633
+ throw errors.INVALID_ARG_VALUE('Missing UNC resource path')
634
+ }
635
+
636
+ if (hostEnd === 2) {
637
+ throw errors.INVALID_ARG_VALUE('Empty UNC servername')
638
+ }
639
+
640
+ const hostname = truncateHost('file', resolved.slice(hostStart, hostEnd))
641
+
642
+ return new URL('file://' + hostname + encodeFilePath(resolved.slice(hostEnd)))
643
+ }
604
644
 
605
645
  if (pathname[pathname.length - 1] === '/') {
606
646
  resolved += '/'
@@ -608,23 +648,7 @@ exports.pathToFileURL = function pathToFileURL(pathname) {
608
648
  resolved += '\\'
609
649
  }
610
650
 
611
- // Paths hardly ever contain any of these, so one pass to rule them out is
612
- // cheaper than the six or seven replacements it stands in for.
613
- if (reserved.test(resolved)) {
614
- resolved = resolved
615
- .replaceAll('%', '%25') // Must be first
616
- .replaceAll('#', '%23')
617
- .replaceAll('?', '%3f')
618
- .replaceAll('\n', '%0a')
619
- .replaceAll('\r', '%0d')
620
- .replaceAll('\t', '%09')
621
-
622
- if (!isWindows) {
623
- resolved = resolved.replaceAll('\\', '%5c')
624
- }
625
- }
626
-
627
- return new URL('file:' + resolved)
651
+ return new URL('file:' + encodeFilePath(resolved))
628
652
  }
629
653
 
630
654
  exports.format = function format(parts) {
@@ -30,4 +30,8 @@ module.exports = class URLError extends Error {
30
30
  static INVALID_FILE_URL_PATH(msg = 'Invalid file: URL path') {
31
31
  return new URLError(msg, URLError.INVALID_FILE_URL_PATH)
32
32
  }
33
+
34
+ static INVALID_ARG_VALUE(msg = 'Invalid argument value') {
35
+ return new URLError(msg, URLError.INVALID_ARG_VALUE)
36
+ }
33
37
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-url",
3
- "version": "2.5.4",
3
+ "version": "2.5.5",
4
4
  "description": "WHATWG URL implementation for JavaScript",
5
5
  "exports": {
6
6
  "./package": "./package.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-mac2-driver",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "XCTest-based Appium driver for macOS apps automation",
5
5
  "keywords": [
6
6
  "XCTest",
@@ -69,7 +69,8 @@
69
69
  ],
70
70
  "mainClass": "Mac2Driver",
71
71
  "scripts": {
72
- "open-wda": "./scripts/open-wda.mjs"
72
+ "open-wda": "./scripts/open-wda.mjs",
73
+ "setup-accessibility": "./scripts/setup-accessibility.mjs"
73
74
  },
74
75
  "doctor": {
75
76
  "checks": [
@@ -0,0 +1,175 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ import {logger} from 'appium/support.js';
7
+ import {exec} from 'teen_process';
8
+
9
+ const RUNNER_NAME = 'WebDriverAgentRunner-Runner.app';
10
+ const RUNNER_SCHEME = 'WebDriverAgentRunner';
11
+ // Keep the build arguments in sync with the driver, so it reuses this build without re-signing it
12
+ const DISABLE_STORE_ARG = 'COMPILER_INDEX_STORE_ENABLE=NO';
13
+ const XCODEPROJ_NAME = 'WebDriverAgentMac.xcodeproj';
14
+ const DERIVED_DATA_ROOT = path.join(os.homedir(), 'Library', 'Developer', 'Xcode', 'DerivedData');
15
+ const SETTINGS_URL = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility';
16
+ const NO_OPEN_FLAG = '--no-open';
17
+ const RUNNER_PATH_ENV = 'WDA_RUNNER_PATH';
18
+ const KEEP_RUNNING_FLAG = '--keep-running';
19
+ const RUNNER_EXECUTABLE_FRAGMENT = `/${RUNNER_NAME}/Contents/MacOS/`;
20
+
21
+ const log = logger.getLogger('WDA');
22
+
23
+ async function exists(p) {
24
+ try {
25
+ await fs.access(p);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Xcode stores the project path of each derived data folder in its info.plist
34
+ */
35
+ async function readWorkspacePath(derivedDataDir) {
36
+ try {
37
+ const {stdout} = await exec('plutil', [
38
+ '-extract',
39
+ 'WorkspacePath',
40
+ 'raw',
41
+ '-o',
42
+ '-',
43
+ path.join(derivedDataDir, 'info.plist'),
44
+ ]);
45
+ return stdout.trim();
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Finds the runner app built from the WebDriverAgent project of this driver.
53
+ * The newest one wins if there are many.
54
+ */
55
+ async function findRunner(projectPath) {
56
+ let entries;
57
+ try {
58
+ entries = await fs.readdir(DERIVED_DATA_ROOT);
59
+ } catch {
60
+ return null;
61
+ }
62
+ const candidates = [];
63
+ for (const entry of entries.filter((name) => name.startsWith('WebDriverAgentMac-'))) {
64
+ const derivedDataDir = path.join(DERIVED_DATA_ROOT, entry);
65
+ const runnerPath = path.join(derivedDataDir, 'Build', 'Products', 'Debug', RUNNER_NAME);
66
+ if (!(await exists(runnerPath)) || (await readWorkspacePath(derivedDataDir)) !== projectPath) {
67
+ continue;
68
+ }
69
+ candidates.push({runnerPath, mtimeMs: (await fs.stat(runnerPath)).mtimeMs});
70
+ }
71
+ return candidates.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.runnerPath ?? null;
72
+ }
73
+
74
+ /**
75
+ * Stops the runner and the xcodebuild process that hosts it. macOS evaluates the permission
76
+ * when a process starts, so a runner started before the grant stays untrusted.
77
+ * Appium itself keeps running, but an active session loses its runner.
78
+ */
79
+ async function stopRunningRunners() {
80
+ const {stdout} = await exec('ps', ['-axo', 'pid=,command=']);
81
+ const pids = [];
82
+ for (const line of stdout.split('\n')) {
83
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
84
+ if (!match) {
85
+ continue;
86
+ }
87
+ const pid = Number(match[1]);
88
+ const command = match[2];
89
+ const isRunner = command.includes(RUNNER_EXECUTABLE_FRAGMENT);
90
+ const isHost =
91
+ /(^|\/)xcodebuild\s/.test(command) &&
92
+ command.includes('test-without-building') &&
93
+ command.includes(`-scheme ${RUNNER_SCHEME}`);
94
+ if ((isRunner || isHost) && pid !== process.pid && pid !== process.ppid) {
95
+ pids.push(pid);
96
+ }
97
+ }
98
+ for (const pid of pids) {
99
+ try {
100
+ process.kill(pid, 'SIGTERM');
101
+ } catch (e) {
102
+ log.warn(`Cannot stop the process ${pid}: ${e.message}`);
103
+ }
104
+ }
105
+ if (pids.length > 0) {
106
+ log.info(`Stopped the running WebDriverAgent processes: ${pids.join(', ')}`);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Builds the runner the same way the driver does
112
+ */
113
+ async function buildRunner(projectPath) {
114
+ log.info(`Building ${RUNNER_NAME}. This may take a few minutes`);
115
+ try {
116
+ await exec(
117
+ 'xcodebuild',
118
+ ['build-for-testing', '-project', projectPath, '-scheme', RUNNER_SCHEME, DISABLE_STORE_ARG],
119
+ {cwd: path.dirname(projectPath)},
120
+ );
121
+ } catch (e) {
122
+ const output = String(e.stdout ?? '')
123
+ .trim()
124
+ .split('\n')
125
+ .slice(-20)
126
+ .join('\n');
127
+ throw new Error(`Cannot build ${RUNNER_NAME}: ${e.stderr || e.message}\n${output}`, {cause: e});
128
+ }
129
+ }
130
+
131
+ async function setupAccessibility() {
132
+ const args = process.argv.slice(2);
133
+ const shouldOpen = !args.includes(NO_OPEN_FLAG);
134
+ const shouldStop = !args.includes(KEEP_RUNNING_FLAG);
135
+ const explicitPath = args.find((arg) => !arg.startsWith('--')) ?? process.env[RUNNER_PATH_ENV];
136
+
137
+ if (shouldStop) {
138
+ await stopRunningRunners();
139
+ }
140
+
141
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
142
+ const projectPath = path.resolve(__dirname, '..', 'WebDriverAgentMac', XCODEPROJ_NAME);
143
+ let runnerPath = explicitPath ? path.resolve(explicitPath) : await findRunner(projectPath);
144
+ if (!runnerPath && !explicitPath) {
145
+ await buildRunner(projectPath);
146
+ runnerPath = await findRunner(projectPath);
147
+ }
148
+ if (!runnerPath || !(await exists(runnerPath))) {
149
+ log.error(
150
+ `Cannot find ${RUNNER_NAME}${explicitPath ? ` at '${runnerPath}'` : ` built from '${projectPath}'`}. ` +
151
+ `If your Xcode derived data folder is not in '${DERIVED_DATA_ROOT}', ` +
152
+ `set the ${RUNNER_PATH_ENV} environment variable to the full path to ${RUNNER_NAME}.`,
153
+ );
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+
158
+ log.info(`WebDriverAgent runner: '${runnerPath}'`);
159
+ log.info(
160
+ `Add the runner to System Settings > Privacy & Security > Accessibility and turn it on. ` +
161
+ `Then restart your Appium session. Do not rebuild the runner after granting the permission, ` +
162
+ `because the grant may be tied to the exact build.`,
163
+ );
164
+ if (!shouldOpen) {
165
+ return;
166
+ }
167
+ // Selects the runner in Finder, so it can be dragged into the Accessibility list
168
+ await exec('open', ['-R', runnerPath]);
169
+ await exec('open', [SETTINGS_URL]);
170
+ }
171
+
172
+ setupAccessibility().catch((e) => {
173
+ log.error(e.message);
174
+ process.exitCode = 1;
175
+ });