appium-mac2-driver 4.3.6 → 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 (106) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/WebDriverAgentMac/IntegrationTests/AMSessionTests.m +14 -0
  3. package/WebDriverAgentMac/IntegrationTests/AMWindowTests.m +427 -0
  4. package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMWindowCommands.m +63 -0
  5. package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +2 -0
  6. package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBSessionCommands.m +17 -5
  7. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.h +3 -0
  8. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.m +13 -0
  9. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBSession.m +5 -1
  10. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +4 -0
  11. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +23 -0
  12. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +18 -0
  13. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +43 -28
  14. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.h +111 -0
  15. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.m +780 -0
  16. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.h +61 -0
  17. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.m +108 -0
  18. package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +20 -0
  19. package/build/lib/commands/native-record-screen.d.ts +2 -0
  20. package/build/lib/commands/native-record-screen.d.ts.map +1 -1
  21. package/build/lib/commands/native-record-screen.js.map +1 -1
  22. package/build/lib/commands/windows.d.ts +22 -0
  23. package/build/lib/commands/windows.d.ts.map +1 -0
  24. package/build/lib/commands/windows.js +32 -0
  25. package/build/lib/commands/windows.js.map +1 -0
  26. package/build/lib/driver.d.ts +15 -0
  27. package/build/lib/driver.d.ts.map +1 -1
  28. package/build/lib/driver.js +55 -32
  29. package/build/lib/driver.js.map +1 -1
  30. package/build/lib/execute-method-map.d.ts +9 -0
  31. package/build/lib/execute-method-map.d.ts.map +1 -1
  32. package/build/lib/execute-method-map.js +9 -0
  33. package/build/lib/execute-method-map.js.map +1 -1
  34. package/build/lib/session-claim-handler.d.ts +41 -0
  35. package/build/lib/session-claim-handler.d.ts.map +1 -0
  36. package/build/lib/session-claim-handler.js +236 -0
  37. package/build/lib/session-claim-handler.js.map +1 -0
  38. package/build/lib/types.d.ts +26 -0
  39. package/build/lib/types.d.ts.map +1 -1
  40. package/build/lib/wda-mac.d.ts +6 -2
  41. package/build/lib/wda-mac.d.ts.map +1 -1
  42. package/build/lib/wda-mac.js +39 -9
  43. package/build/lib/wda-mac.js.map +1 -1
  44. package/lib/commands/native-record-screen.ts +2 -0
  45. package/lib/commands/windows.ts +44 -0
  46. package/lib/driver.ts +59 -31
  47. package/lib/execute-method-map.ts +9 -0
  48. package/lib/session-claim-handler.ts +306 -0
  49. package/lib/types.ts +28 -0
  50. package/lib/wda-mac.ts +41 -10
  51. package/node_modules/appium-xcode/CHANGELOG.md +7 -0
  52. package/node_modules/appium-xcode/package.json +4 -4
  53. package/node_modules/async-lock/AUTHORS +21 -0
  54. package/node_modules/async-lock/History.md +132 -0
  55. package/node_modules/async-lock/LICENSE +22 -0
  56. package/node_modules/async-lock/README.md +177 -0
  57. package/node_modules/async-lock/index.js +2 -0
  58. package/node_modules/async-lock/lib/index.js +312 -0
  59. package/node_modules/async-lock/package.json +48 -0
  60. package/node_modules/bare-fs/binding.c +1 -1
  61. package/node_modules/bare-fs/index.js +30 -6
  62. package/node_modules/bare-fs/package.json +1 -1
  63. package/node_modules/bare-fs/prebuilds/android-arm/bare-fs.bare +0 -0
  64. package/node_modules/bare-fs/prebuilds/android-arm64/bare-fs.bare +0 -0
  65. package/node_modules/bare-fs/prebuilds/android-ia32/bare-fs.bare +0 -0
  66. package/node_modules/bare-fs/prebuilds/android-x64/bare-fs.bare +0 -0
  67. package/node_modules/bare-fs/prebuilds/darwin-arm64/bare-fs.bare +0 -0
  68. package/node_modules/bare-fs/prebuilds/darwin-x64/bare-fs.bare +0 -0
  69. package/node_modules/bare-fs/prebuilds/ios-arm64/bare-fs.bare +0 -0
  70. package/node_modules/bare-fs/prebuilds/ios-arm64-simulator/bare-fs.bare +0 -0
  71. package/node_modules/bare-fs/prebuilds/ios-x64-simulator/bare-fs.bare +0 -0
  72. package/node_modules/bare-fs/prebuilds/linux-arm64/bare-fs.bare +0 -0
  73. package/node_modules/bare-fs/prebuilds/linux-x64/bare-fs.bare +0 -0
  74. package/node_modules/bare-fs/prebuilds/win32-arm64/bare-fs.bare +0 -0
  75. package/node_modules/bare-fs/prebuilds/win32-x64/bare-fs.bare +0 -0
  76. package/node_modules/bare-url/README.md +4 -0
  77. package/node_modules/bare-url/index.d.ts +3 -0
  78. package/node_modules/bare-url/index.js +42 -18
  79. package/node_modules/bare-url/lib/errors.js +4 -0
  80. package/node_modules/bare-url/package.json +1 -1
  81. package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
  82. package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
  83. package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
  84. package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
  85. package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
  86. package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
  87. package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
  88. package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
  89. package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
  90. package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
  91. package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
  92. package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
  93. package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
  94. package/node_modules/readable-stream/lib/internal/shims/process.js +19 -0
  95. package/node_modules/readable-stream/lib/internal/streams/destroy.js +1 -1
  96. package/node_modules/readable-stream/lib/internal/streams/duplexify.js +1 -1
  97. package/node_modules/readable-stream/lib/internal/streams/end-of-stream.js +1 -1
  98. package/node_modules/readable-stream/lib/internal/streams/from.js +1 -1
  99. package/node_modules/readable-stream/lib/internal/streams/legacy.js +6 -1
  100. package/node_modules/readable-stream/lib/internal/streams/pipeline.js +1 -1
  101. package/node_modules/readable-stream/lib/internal/streams/readable.js +1 -1
  102. package/node_modules/readable-stream/lib/internal/streams/writable.js +1 -1
  103. package/node_modules/readable-stream/package.json +3 -2
  104. package/node_modules/tar-stream/package.json +7 -7
  105. package/package.json +6 -3
  106. package/scripts/setup-accessibility.mjs +175 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [4.5.0](https://github.com/appium/appium-mac2-driver/compare/v4.4.0...v4.5.0) (2026-10-19)
2
+
3
+ ### Features
4
+
5
+ * add window listing and adjustment extension methods ([#428](https://github.com/appium/appium-mac2-driver/issues/428)) ([42759b6](https://github.com/appium/appium-mac2-driver/commit/42759b603ade35832369b22f790ebe083bff2f3d))
6
+
7
+ ## [4.4.0](https://github.com/appium/appium-mac2-driver/compare/v4.3.6...v4.4.0) (2026-10-11)
8
+
9
+ ### Features
10
+
11
+ * enforce a single session, a new session replaces the previous one ([#426](https://github.com/appium/appium-mac2-driver/issues/426)) ([fb10372](https://github.com/appium/appium-mac2-driver/commit/fb10372c16d861fb7ff9463b345ba6fcb3f67387))
12
+
1
13
  ## [4.3.6](https://github.com/appium/appium-mac2-driver/compare/v4.3.5...v4.3.6) (2026-10-02)
2
14
 
3
15
  ### Bug Fixes
@@ -43,6 +43,20 @@ static NSString *const SETTINGS_BUNDLE_ID = @"com.apple.systempreferences";
43
43
  [super tearDown];
44
44
  }
45
45
 
46
+ - (void)testNewSessionOverridesThePreviousOne
47
+ {
48
+ NSString *oldId = self.session.identifier;
49
+ FBSession *newSession = [FBSession initWithApplication:nil];
50
+ XCTAssertNotEqualObjects(oldId, newSession.identifier);
51
+ XCTAssertEqual(FBSession.activeSession, newSession);
52
+ XCTAssertNil([FBSession sessionWithIdentifier:oldId]);
53
+ // Killing the stale session must not drop the active one
54
+ [self.session kill];
55
+ XCTAssertEqual(FBSession.activeSession, newSession);
56
+ [newSession kill];
57
+ XCTAssertNil(FBSession.activeSession);
58
+ }
59
+
46
60
  - (void)testSettingsAppCanBeOpenedInScopeOfTheCurrentSession
47
61
  {
48
62
  [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID
@@ -0,0 +1,427 @@
1
+ /*
2
+ * Licensed under the Apache License, Version 2.0 (the "License");
3
+ * you may not use this file except in compliance with the License.
4
+ * See the NOTICE file distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import <XCTest/XCTest.h>
18
+
19
+ #import "AMIntegrationTestCase.h"
20
+ #import "AMScreenUtils.h"
21
+ #import "AMSnapshotUtils.h"
22
+ #import "AMWindowUtils.h"
23
+ #import "AMXCTAccessibility.h"
24
+ #import "FBTestMacros.h"
25
+
26
+ static const CGFloat AM_GEOMETRY_ACCURACY = 1.0;
27
+
28
+ /**
29
+ The windows are inspected and changed through the XCTest accessibility interface, which does not
30
+ need the Accessibility permission, except for the window geometry. AMWindowAccessibilityApiTests
31
+ runs the same tests using the Accessibility API only.
32
+ */
33
+ @interface AMWindowTests : AMIntegrationTestCase
34
+ - (BOOL)usesUnprivilegedRoutes;
35
+ @end
36
+
37
+ @implementation AMWindowTests
38
+
39
+ - (BOOL)usesUnprivilegedRoutes
40
+ {
41
+ return YES;
42
+ }
43
+
44
+ - (void)setUp
45
+ {
46
+ [super setUp];
47
+ AMWindowUtils.usesUnprivilegedRoutes = [self usesUnprivilegedRoutes];
48
+ [self launchApplication];
49
+ }
50
+
51
+ - (void)tearDown
52
+ {
53
+ // Leave the window in a regular state for the other tests
54
+ NSDictionary *normal = @{@"state": AMWindowStateNormal};
55
+ [AMWindowUtils adjustMinimizedWindowOfApplication:self.testedApplication withArguments:normal error:nil];
56
+ XCUIElement *window = [AMWindowUtils mainWindowOfApplication:self.testedApplication];
57
+ if (nil != window) {
58
+ [AMWindowUtils adjustWindow:window inApplication:self.testedApplication withArguments:normal error:nil];
59
+ }
60
+ AMWindowUtils.usesUnprivilegedRoutes = YES;
61
+ [super tearDown];
62
+ }
63
+
64
+ // Changing states needs the XCTest accessibility interface, or the Accessibility permission if it is not used
65
+ - (void)skipUnlessStateRoutesAvailable
66
+ {
67
+ if ([self usesUnprivilegedRoutes]) {
68
+ XCTSkipUnless(AMXCTAccessibility.isAvailable, @"The XCTest accessibility interface is not available");
69
+ } else {
70
+ [self skipUnlessAccessibilityTrusted];
71
+ }
72
+ }
73
+
74
+ #pragma mark - Helpers
75
+
76
+ - (XCUIElement *)mainWindow
77
+ {
78
+ XCUIElement *window = [AMWindowUtils mainWindowOfApplication:self.testedApplication];
79
+ XCTAssertNotNil(window);
80
+ return window;
81
+ }
82
+
83
+ - (AMScreenProperties *)mainDisplay
84
+ {
85
+ for (AMScreenProperties *screen in AMListScreens()) {
86
+ if (screen.isMain) {
87
+ return screen;
88
+ }
89
+ }
90
+ XCTFail(@"No main display is reported");
91
+ return nil;
92
+ }
93
+
94
+ - (NSDictionary<NSString *, id> *)adjustWindow:(XCUIElement *)window arguments:(NSDictionary<NSString *, id> *)arguments
95
+ {
96
+ NSError *error = nil;
97
+ NSDictionary *result = [AMWindowUtils adjustWindow:window inApplication:self.testedApplication withArguments:arguments error:&error];
98
+ XCTAssertNotNil(result, @"The call has failed: %@", error);
99
+ return result;
100
+ }
101
+
102
+ - (NSDictionary<NSString *, id> *)adjustMainWindowWithArguments:(NSDictionary<NSString *, id> *)arguments
103
+ {
104
+ return [self adjustWindow:[self mainWindow] arguments:arguments];
105
+ }
106
+
107
+ - (void)assertInvalidArgumentWithMessage:(NSString *)fragment forArguments:(NSDictionary<NSString *, id> *)arguments
108
+ {
109
+ [self assertInvalidArgumentWithMessage:fragment forArguments:arguments window:[self mainWindow]];
110
+ }
111
+
112
+ - (void)assertInvalidArgumentWithMessage:(NSString *)fragment
113
+ forArguments:(NSDictionary<NSString *, id> *)arguments
114
+ window:(XCUIElement *)window
115
+ {
116
+ NSError *error = nil;
117
+ XCTAssertNil([AMWindowUtils adjustWindow:window inApplication:self.testedApplication withArguments:arguments error:&error]);
118
+ XCTAssertEqualObjects(error.domain, AMWindowErrorDomain);
119
+ XCTAssertEqual(error.code, AMWindowErrorInvalidArgument);
120
+ XCTAssertTrue([error.localizedDescription containsString:fragment], @"Unexpected message: %@", error.localizedDescription);
121
+ }
122
+
123
+ - (CGRect)rectFromInfo:(NSDictionary<NSString *, id> *)info
124
+ {
125
+ NSDictionary<NSString *, NSNumber *> *rect = info[@"rect"];
126
+ return CGRectMake(rect[@"x"].doubleValue, rect[@"y"].doubleValue, rect[@"width"].doubleValue, rect[@"height"].doubleValue);
127
+ }
128
+
129
+ - (void)assertRect:(CGRect)actual equalsRect:(CGRect)expected
130
+ {
131
+ XCTAssertEqualWithAccuracy(actual.origin.x, expected.origin.x, AM_GEOMETRY_ACCURACY);
132
+ XCTAssertEqualWithAccuracy(actual.origin.y, expected.origin.y, AM_GEOMETRY_ACCURACY);
133
+ XCTAssertEqualWithAccuracy(actual.size.width, expected.size.width, AM_GEOMETRY_ACCURACY);
134
+ XCTAssertEqualWithAccuracy(actual.size.height, expected.size.height, AM_GEOMETRY_ACCURACY);
135
+ }
136
+
137
+ - (void)assertRect:(CGRect)rect isWithinRect:(CGRect)container
138
+ {
139
+ XCTAssertTrue(CGRectContainsRect(CGRectInset(container, -AM_GEOMETRY_ACCURACY, -AM_GEOMETRY_ACCURACY), rect),
140
+ @"%@ is not within %@", NSStringFromRect(rect), NSStringFromRect(container));
141
+ }
142
+
143
+ #pragma mark - Displays
144
+
145
+ - (void)testDisplaysReportTheirBounds
146
+ {
147
+ AMScreenProperties *display = [self mainDisplay];
148
+ XCTAssertTrue(display.bounds.size.width > 0 && display.bounds.size.height > 0);
149
+ // The main display is the origin of the global coordinate space
150
+ XCTAssertEqualWithAccuracy(display.bounds.origin.x, 0, AM_GEOMETRY_ACCURACY);
151
+ XCTAssertEqualWithAccuracy(display.bounds.origin.y, 0, AM_GEOMETRY_ACCURACY);
152
+ // The menu bar and the Dock are excluded
153
+ [self assertRect:display.visibleBounds isWithinRect:display.bounds];
154
+ }
155
+
156
+ #pragma mark - Listing
157
+
158
+ - (void)testWindowsAreListed
159
+ {
160
+ NSArray<XCUIElement *> *windows = [AMWindowUtils windowsOfApplication:self.testedApplication];
161
+ XCTAssertTrue(windows.count > 0);
162
+ NSDictionary *info = [AMWindowUtils infoForWindow:windows.firstObject];
163
+ XCTAssertNotNil(info[@"title"]);
164
+ CGRect rect = [self rectFromInfo:info];
165
+ XCTAssertTrue(rect.size.width > 0 && rect.size.height > 0);
166
+ XCTAssertEqualObjects(info[@"displayId"], @([self mainDisplay].identifier));
167
+ }
168
+
169
+ - (void)testWindowTraitsAreListed
170
+ {
171
+ [self skipUnlessStateRoutesAvailable];
172
+
173
+ NSDictionary *info = [AMWindowUtils infoForWindow:[self mainWindow]];
174
+ for (NSString *trait in @[@"isMain", @"isFocused", @"isMinimized", @"isFullscreen"]) {
175
+ XCTAssertTrue([info[trait] isKindOfClass:NSNumber.class], @"'%@' must be a boolean: %@", trait, info);
176
+ }
177
+ XCTAssertTrue([info[@"isMain"] boolValue]);
178
+ XCTAssertFalse([info[@"isMinimized"] boolValue]);
179
+ XCTAssertFalse([info[@"isFullscreen"] boolValue]);
180
+ // These can only be asked through the Accessibility API
181
+ if (AMSnapshotUtils.isAccessibilityTrusted) {
182
+ XCTAssertTrue([info[@"isMovable"] boolValue]);
183
+ XCTAssertTrue([info[@"isResizable"] boolValue]);
184
+ } else {
185
+ XCTAssertEqualObjects(info[@"isMovable"], NSNull.null);
186
+ XCTAssertEqualObjects(info[@"isResizable"], NSNull.null);
187
+ }
188
+ }
189
+
190
+ #pragma mark - Argument validation
191
+
192
+ - (void)testAdjustingRequiresAtLeastOneArgument
193
+ {
194
+ [self assertInvalidArgumentWithMessage:@"At least one of" forArguments:@{}];
195
+ [self assertInvalidArgumentWithMessage:@"At least one of" forArguments:@{@"windowId": @"foo"}];
196
+ }
197
+
198
+ - (void)testAdjustingRejectsUnknownState
199
+ {
200
+ [self assertInvalidArgumentWithMessage:@"'state' must be one of" forArguments:@{@"state": @"foo"}];
201
+ }
202
+
203
+ - (void)testAdjustingRejectsInvalidGeometry
204
+ {
205
+ [self assertInvalidArgumentWithMessage:@"'width'" forArguments:@{@"width": @0}];
206
+ [self assertInvalidArgumentWithMessage:@"'height'" forArguments:@{@"height": @(-10)}];
207
+ [self assertInvalidArgumentWithMessage:@"'x'" forArguments:@{@"x": @"foo"}];
208
+ }
209
+
210
+ - (void)testAdjustingRejectsStateCombinedWithGeometry
211
+ {
212
+ [self assertInvalidArgumentWithMessage:@"cannot be combined"
213
+ forArguments:@{@"state": AMWindowStateMinimized, @"x": @10}];
214
+ [self assertInvalidArgumentWithMessage:@"cannot be combined"
215
+ forArguments:@{@"state": AMWindowStateFullscreen, @"displayId": @([self mainDisplay].identifier)}];
216
+ [self assertInvalidArgumentWithMessage:@"cannot be combined"
217
+ forArguments:@{@"state": AMWindowStateMaximized, @"width": @500}];
218
+ }
219
+
220
+ - (void)testAdjustingRejectsUnknownDisplay
221
+ {
222
+ [self assertInvalidArgumentWithMessage:@"is unknown" forArguments:@{@"displayId": @999999}];
223
+ }
224
+
225
+ - (void)testAdjustingRejectsMalformedDisplayId
226
+ {
227
+ [self assertInvalidArgumentWithMessage:@"'displayId' must be a number" forArguments:@{@"displayId": @{}}];
228
+ [self assertInvalidArgumentWithMessage:@"'displayId' must be a number" forArguments:@{@"displayId": @"foo"}];
229
+ }
230
+
231
+ - (void)testAdjustingRejectsNonWindowElements
232
+ {
233
+ XCUIElement *button = self.testedApplication.buttons.firstMatch;
234
+ XCTAssertTrue([button waitForExistenceWithTimeout:5.0]);
235
+ [self assertInvalidArgumentWithMessage:@"is not a window" forArguments:@{@"x": @10} window:button];
236
+ }
237
+
238
+ #pragma mark - Adjusting (requires the Accessibility permission)
239
+
240
+ - (void)testWindowCanBeResizedAndMoved
241
+ {
242
+ [self skipUnlessAccessibilityTrusted];
243
+
244
+ XCUIElement *window = [self mainWindow];
245
+ CGRect original = [self rectFromInfo:[AMWindowUtils infoForWindow:window]];
246
+ CGRect expected = CGRectMake(original.origin.x + 30, original.origin.y + 30,
247
+ original.size.width + 20, original.size.height + 20);
248
+ NSDictionary *result = [self adjustWindow:window arguments:@{
249
+ @"x": @(expected.origin.x),
250
+ @"y": @(expected.origin.y),
251
+ @"width": @(expected.size.width),
252
+ @"height": @(expected.size.height),
253
+ }];
254
+ [self assertRect:[self rectFromInfo:result] equalsRect:expected];
255
+ // The change is observable from the outside
256
+ [self assertRect:[self rectFromInfo:[AMWindowUtils infoForWindow:window]] equalsRect:expected];
257
+ [self assertRect:window.frame equalsRect:expected];
258
+ }
259
+
260
+ - (void)testOmittedGeometryIsKept
261
+ {
262
+ [self skipUnlessAccessibilityTrusted];
263
+
264
+ XCUIElement *window = [self mainWindow];
265
+ CGRect original = [self rectFromInfo:[AMWindowUtils infoForWindow:window]];
266
+ NSDictionary *result = [self adjustWindow:window arguments:@{@"width": @(original.size.width + 20)}];
267
+ [self assertRect:[self rectFromInfo:result]
268
+ equalsRect:CGRectMake(original.origin.x, original.origin.y, original.size.width + 20, original.size.height)];
269
+ }
270
+
271
+ - (void)testWindowCanBeMovedToDisplay
272
+ {
273
+ [self skipUnlessAccessibilityTrusted];
274
+
275
+ AMScreenProperties *display = [self mainDisplay];
276
+ NSDictionary *result = [self adjustMainWindowWithArguments:@{
277
+ @"displayId": @(display.identifier),
278
+ @"x": @80,
279
+ @"y": @120,
280
+ }];
281
+ CGRect rect = [self rectFromInfo:result];
282
+ // Coordinates are relative to the display
283
+ XCTAssertEqualWithAccuracy(rect.origin.x, display.bounds.origin.x + 80, AM_GEOMETRY_ACCURACY);
284
+ XCTAssertEqualWithAccuracy(rect.origin.y, display.bounds.origin.y + 120, AM_GEOMETRY_ACCURACY);
285
+ XCTAssertEqualObjects(result[@"displayId"], @(display.identifier));
286
+ }
287
+
288
+ - (void)testWindowIsKeptWithinDisplay
289
+ {
290
+ [self skipUnlessAccessibilityTrusted];
291
+
292
+ AMScreenProperties *display = [self mainDisplay];
293
+ NSDictionary *result = [self adjustMainWindowWithArguments:@{
294
+ @"displayId": @(display.identifier),
295
+ @"x": @(display.bounds.size.width * 10),
296
+ @"y": @(display.bounds.size.height * 10),
297
+ }];
298
+ [self assertRect:[self rectFromInfo:result] isWithinRect:display.bounds];
299
+ }
300
+
301
+ - (void)testWindowCanBeMaximized
302
+ {
303
+ [self skipUnlessAccessibilityTrusted];
304
+
305
+ AMScreenProperties *display = [self mainDisplay];
306
+ CGRect original = [self rectFromInfo:[AMWindowUtils infoForWindow:[self mainWindow]]];
307
+ CGRect rect = [self rectFromInfo:[self adjustMainWindowWithArguments:@{@"state": AMWindowStateMaximized}]];
308
+ XCTAssertTrue(rect.size.width * rect.size.height >= original.size.width * original.size.height,
309
+ @"%@ must not be smaller than %@", NSStringFromRect(rect), NSStringFromRect(original));
310
+ // Fills the display, except the menu bar and the Dock
311
+ [self assertRect:rect isWithinRect:display.bounds];
312
+ XCTAssertTrue(rect.size.width >= display.visibleBounds.size.width - AM_GEOMETRY_ACCURACY);
313
+
314
+ // Maximizing is idempotent
315
+ NSDictionary *repeated = [self adjustMainWindowWithArguments:@{@"state": AMWindowStateMaximized}];
316
+ [self assertRect:[self rectFromInfo:repeated] equalsRect:rect];
317
+ }
318
+
319
+ - (void)testWindowCanBeMinimizedAndRestored
320
+ {
321
+ [self skipUnlessStateRoutesAvailable];
322
+
323
+ XCUIApplication *application = self.testedApplication;
324
+ CGRect original = [self rectFromInfo:[AMWindowUtils infoForWindow:[self mainWindow]]];
325
+ NSDictionary *minimized = [self adjustMainWindowWithArguments:@{@"state": AMWindowStateMinimized}];
326
+ XCTAssertEqualObjects(minimized[@"isMinimized"], @YES);
327
+ FBAssertWaitTillBecomesTrue(1 == [AMWindowUtils minimizedWindowsCountOfApplication:application]);
328
+
329
+ // Minimized windows are not a part of the element hierarchy
330
+ NSError *error = nil;
331
+ NSDictionary *restored = [AMWindowUtils adjustMinimizedWindowOfApplication:application
332
+ withArguments:@{@"state": AMWindowStateNormal}
333
+ error:&error];
334
+ XCTAssertNotNil(restored, @"The call has failed: %@", error);
335
+ XCTAssertEqualObjects(restored[@"isMinimized"], @NO);
336
+ [self assertRect:[self rectFromInfo:restored] equalsRect:original];
337
+ FBAssertWaitTillBecomesTrue(0 == [AMWindowUtils minimizedWindowsCountOfApplication:application]);
338
+ FBAssertWaitTillBecomesTrue(nil != [AMWindowUtils mainWindowOfApplication:application]);
339
+ }
340
+
341
+ - (void)testMinimizedWindowIsRestoredByGeometryChange
342
+ {
343
+ [self skipUnlessAccessibilityTrusted];
344
+
345
+ XCUIApplication *application = self.testedApplication;
346
+ CGRect original = [self rectFromInfo:[AMWindowUtils infoForWindow:[self mainWindow]]];
347
+ [self adjustMainWindowWithArguments:@{@"state": AMWindowStateMinimized}];
348
+ FBAssertWaitTillBecomesTrue(1 == [AMWindowUtils minimizedWindowsCountOfApplication:application]);
349
+
350
+ NSError *error = nil;
351
+ NSDictionary *result = [AMWindowUtils adjustMinimizedWindowOfApplication:application
352
+ withArguments:@{@"width": @(original.size.width + 10)}
353
+ error:&error];
354
+ XCTAssertNotNil(result, @"The call has failed: %@", error);
355
+ XCTAssertEqualObjects(result[@"isMinimized"], @NO);
356
+ XCTAssertEqualWithAccuracy([self rectFromInfo:result].size.width, original.size.width + 10, AM_GEOMETRY_ACCURACY);
357
+ }
358
+
359
+ - (void)testRestoringRequiresMinimizedWindow
360
+ {
361
+ [self skipUnlessStateRoutesAvailable];
362
+
363
+ NSError *error = nil;
364
+ XCTAssertNil([AMWindowUtils adjustMinimizedWindowOfApplication:self.testedApplication
365
+ withArguments:@{@"state": AMWindowStateNormal}
366
+ error:&error]);
367
+ XCTAssertEqual(error.code, AMWindowErrorNotFound);
368
+ }
369
+
370
+ - (void)testGeometryRequiresAccessibilityPermission
371
+ {
372
+ XCTSkipIf(AMSnapshotUtils.isAccessibilityTrusted, @"The process is trusted for the Accessibility API");
373
+
374
+ NSError *error = nil;
375
+ XCTAssertNil([AMWindowUtils adjustWindow:[self mainWindow]
376
+ inApplication:self.testedApplication
377
+ withArguments:@{@"width": @600}
378
+ error:&error]);
379
+ XCTAssertEqual(error.code, AMWindowErrorFailure);
380
+ XCTAssertTrue([error.localizedDescription containsString:@"Accessibility"], @"%@", error.localizedDescription);
381
+ }
382
+
383
+ - (void)testWindowCanEnterAndLeaveFullscreen
384
+ {
385
+ [self skipUnlessStateRoutesAvailable];
386
+
387
+ XCUIElement *window = [self mainWindow];
388
+ NSDictionary *fullscreen = [self adjustWindow:window arguments:@{@"state": AMWindowStateFullscreen}];
389
+ XCTAssertEqualObjects(fullscreen[@"isFullscreen"], @YES);
390
+
391
+ NSDictionary *restored = [self adjustWindow:window arguments:@{@"state": AMWindowStateNormal}];
392
+ XCTAssertEqualObjects(restored[@"isFullscreen"], @NO);
393
+ }
394
+
395
+ - (void)testFullscreenWindowCanBeMinimized
396
+ {
397
+ [self skipUnlessStateRoutesAvailable];
398
+
399
+ XCUIApplication *application = self.testedApplication;
400
+ XCUIElement *window = [self mainWindow];
401
+ NSDictionary *fullscreen = [self adjustWindow:window arguments:@{@"state": AMWindowStateFullscreen}];
402
+ XCTAssertEqualObjects(fullscreen[@"isFullscreen"], @YES);
403
+
404
+ NSDictionary *minimized = [self adjustWindow:window arguments:@{@"state": AMWindowStateMinimized}];
405
+ XCTAssertEqualObjects(minimized[@"isMinimized"], @YES);
406
+ XCTAssertEqualObjects(minimized[@"isFullscreen"], @NO);
407
+
408
+ NSError *error = nil;
409
+ XCTAssertNotNil([AMWindowUtils adjustMinimizedWindowOfApplication:application
410
+ withArguments:@{@"state": AMWindowStateNormal}
411
+ error:&error], @"The call has failed: %@", error);
412
+ FBAssertWaitTillBecomesTrue(nil != [AMWindowUtils mainWindowOfApplication:application]);
413
+ }
414
+
415
+ @end
416
+
417
+ @interface AMWindowAccessibilityApiTests : AMWindowTests
418
+ @end
419
+
420
+ @implementation AMWindowAccessibilityApiTests
421
+
422
+ - (BOOL)usesUnprivilegedRoutes
423
+ {
424
+ return NO;
425
+ }
426
+
427
+ @end
@@ -9,6 +9,8 @@
9
9
 
10
10
  #import "AMWindowCommands.h"
11
11
 
12
+ #import "AMWindowUtils.h"
13
+ #import "FBElementCache.h"
12
14
  #import "FBRoute.h"
13
15
  #import "FBRouteRequest.h"
14
16
  #import "FBSession.h"
@@ -23,6 +25,8 @@
23
25
  [[FBRoute POST:@"/window/maximize"] respondWithTarget:self action:@selector(handleMaximizeWindow:)],
24
26
  [[FBRoute POST:@"/window/fullscreen"] respondWithTarget:self action:@selector(handleFullscreenWindow:)],
25
27
  [[FBRoute POST:@"/window/minimize"] respondWithTarget:self action:@selector(handleMinimizeWindow:)],
28
+ [[FBRoute GET:@"/wda/windows"] respondWithTarget:self action:@selector(handleListWindows:)],
29
+ [[FBRoute POST:@"/wda/window/adjust"] respondWithTarget:self action:@selector(handleAdjustWindow:)],
26
30
  ];
27
31
  }
28
32
 
@@ -49,6 +53,65 @@
49
53
  errorMessage:[NSString stringWithFormat:@"%@ window cannot be minimized because the corresponding button is not available", request.session.currentApplication.description]];
50
54
  }
51
55
 
56
+ + (id<FBResponsePayload>)handleListWindows:(FBRouteRequest *)request
57
+ {
58
+ FBElementCache *cache = request.session.elementCache;
59
+ NSMutableArray *result = [NSMutableArray array];
60
+ for (XCUIElement *window in [AMWindowUtils windowsOfApplication:request.session.currentApplication]) {
61
+ NSDictionary *info = [AMWindowUtils infoForWindow:window];
62
+ if (nil != info) {
63
+ [result addObject:[self info:info withWindowId:[cache storeElement:window]]];
64
+ }
65
+ }
66
+ return FBResponseWithObject(result.copy);
67
+ }
68
+
69
+ + (id<FBResponsePayload>)handleAdjustWindow:(FBRouteRequest *)request
70
+ {
71
+ FBElementCache *cache = request.session.elementCache;
72
+ XCUIApplication *application = request.session.currentApplication;
73
+ id rawWindowId = request.arguments[@"windowId"];
74
+ if (nil != rawWindowId && ![rawWindowId isKindOfClass:NSNull.class] && ![rawWindowId isKindOfClass:NSString.class]) {
75
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:
76
+ [NSString stringWithFormat:@"'windowId' must be a string, got '%@'", rawWindowId]
77
+ traceback:nil]);
78
+ }
79
+ NSString *windowId = [rawWindowId isKindOfClass:NSString.class] ? rawWindowId : nil;
80
+ XCUIElement *window = nil != windowId
81
+ ? [cache elementForUUID:windowId]
82
+ : [AMWindowUtils mainWindowOfApplication:application];
83
+ NSError *error = nil;
84
+ NSDictionary *info = nil;
85
+ if (nil != window) {
86
+ info = [AMWindowUtils adjustWindow:window inApplication:application withArguments:request.arguments error:&error];
87
+ } else if (nil == windowId) {
88
+ // There are no visible windows, so a minimized one could be restored
89
+ info = [AMWindowUtils adjustMinimizedWindowOfApplication:application withArguments:request.arguments error:&error];
90
+ }
91
+ NSString *resultWindowId = nil != info && nil != window ? (windowId ?: [cache storeElement:window]) : nil;
92
+ if (nil == info) {
93
+ NSString *message = error.localizedDescription;
94
+ switch (error.code) {
95
+ case AMWindowErrorInvalidArgument:
96
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
97
+ case AMWindowErrorNotFound:
98
+ return FBResponseWithStatus([FBCommandStatus noSuchWindowErrorWithMessage:message traceback:nil]);
99
+ default:
100
+ return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:message traceback:nil]);
101
+ }
102
+ }
103
+ return FBResponseWithObject([self info:info withWindowId:resultWindowId]);
104
+ }
105
+
106
+ #pragma mark - Helpers
107
+
108
+ + (NSDictionary<NSString *, id> *)info:(NSDictionary<NSString *, id> *)info withWindowId:(nullable NSString *)windowId
109
+ {
110
+ NSMutableDictionary *result = [info mutableCopy];
111
+ result[@"windowId"] = windowId ?: NSNull.null;
112
+ return result.copy;
113
+ }
114
+
52
115
  + (id<FBResponsePayload>)performClickWithButtonId:(NSString *)buttonId
53
116
  request:(FBRouteRequest *)request
54
117
  errorMessage:(NSString *)errorMessage
@@ -9,6 +9,7 @@
9
9
 
10
10
  #import "FBDebugCommands.h"
11
11
 
12
+ #import "AMGeometryUtils.h"
12
13
  #import "AMScreenUtils.h"
13
14
  #import "FBRouteRequest.h"
14
15
  #import "FBSession.h"
@@ -65,6 +66,7 @@ static NSString *const SOURCE_FORMAT_DESCRIPTION = @"description";
65
66
  result[[NSString stringWithFormat:@"%lld", screenInfo.identifier]] = @{
66
67
  @"id": @(screenInfo.identifier),
67
68
  @"isMain": @(screenInfo.isMain),
69
+ @"rect": AMCGRectToDict(screenInfo.bounds),
68
70
  };
69
71
  }
70
72
  return FBResponseWithObject(result.copy);
@@ -99,9 +99,15 @@ const static NSString *CAPABILITIES_KEY = @"capabilities";
99
99
  NSString *appPath = requirements[AM_APP_PATH_CAPABILITY];
100
100
  NSString *deepLink = requirements[AM_INITIAL_DEEPLINK_URL_CAPABILITY];
101
101
  BOOL noReset = [requirements[AM_NO_RESET_CAPABILITY] boolValue];
102
- FBSession *session;
102
+
103
+ // XCTest is single-threaded, so there can only be one session. A new one always
104
+ // takes over, and the previous one must be gone before any app gets touched.
105
+ [FBSession.activeSession kill];
106
+
107
+ XCUIApplication *app = nil;
108
+ BOOL didLaunchApp = NO;
103
109
  if (nil == bundleID && nil == appPath && nil == deepLink) {
104
- session = [FBSession initWithApplication:nil];
110
+ // no app to manage
105
111
  } else if (nil != deepLink) {
106
112
  NSURL *url = [NSURL URLWithString:deepLink];
107
113
  NSError *error;
@@ -113,13 +119,12 @@ const static NSString *CAPABILITIES_KEY = @"capabilities";
113
119
  traceback:nil]);
114
120
  }
115
121
  if (nil != bundleID) {
116
- session = [FBSession initWithApplication:[[XCUIApplication alloc] initWithBundleIdentifier:bundleID]];
122
+ app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID];
117
123
  }
118
124
  } else {
119
- XCUIApplication *app = nil != appPath
125
+ app = nil != appPath
120
126
  ? [[XCUIApplication alloc] initWithURL:[NSURL fileURLWithPath:appPath]]
121
127
  : [[XCUIApplication alloc] initWithBundleIdentifier:bundleID];
122
- session = [FBSession initWithApplication:app];
123
128
  if (noReset && app.state > XCUIApplicationStateNotRunning) {
124
129
  [app activate];
125
130
  } else {
@@ -140,6 +145,7 @@ const static NSString *CAPABILITIES_KEY = @"capabilities";
140
145
  }
141
146
  app.launchEnvironment = [launchEnv copy];
142
147
  [app launch];
148
+ didLaunchApp = YES;
143
149
  if (app.state <= XCUIApplicationStateNotRunning) {
144
150
  NSString *message = [NSString stringWithFormat:@"Failed to launch '%@' application", appPath ?: bundleID];
145
151
  return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:message
@@ -149,6 +155,10 @@ const static NSString *CAPABILITIES_KEY = @"capabilities";
149
155
  if (nil != bundleID && nil != appPath) {
150
156
  NSString *realBundleID = app.am_bundleID;
151
157
  if (![realBundleID isEqualToString:bundleID]) {
158
+ // no session owns the app, so don't leave it behind
159
+ if (didLaunchApp) {
160
+ [app terminate];
161
+ }
152
162
  NSString *message = [NSString stringWithFormat:@"The bundle identifier %@ of the '%@' does not match to the one provided in capabilities: %@",
153
163
  realBundleID, appPath, bundleID];
154
164
  return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:message
@@ -156,6 +166,8 @@ const static NSString *CAPABILITIES_KEY = @"capabilities";
156
166
  }
157
167
  }
158
168
  }
169
+ // The session is only registered once startup has fully succeeded
170
+ FBSession *session = [FBSession initWithApplication:app];
159
171
  if (nil != requirements[AM_SKIP_APP_KILL_CAPABILITY]) {
160
172
  session.skipAppTermination = [requirements[AM_SKIP_APP_KILL_CAPABILITY] boolValue];
161
173
  } else if (nil == bundleID) {
@@ -37,6 +37,9 @@ NS_ASSUME_NONNULL_BEGIN
37
37
  + (instancetype)noSuchElementErrorWithMessage:(nullable NSString *)message
38
38
  traceback:(nullable NSString *)traceback;
39
39
 
40
+ + (instancetype)noSuchWindowErrorWithMessage:(nullable NSString *)message
41
+ traceback:(nullable NSString *)traceback;
42
+
40
43
  + (instancetype)invalidElementStateErrorWithMessage:(nullable NSString *)message
41
44
  traceback:(nullable NSString *)traceback;
42
45
 
@@ -21,6 +21,10 @@ static NSString *const FB_NO_SUCH_ELEMENT_ERROR = @"no such element";
21
21
  static const HTTPStatusCode FB_NO_SUCH_ELEMENT_ERROR_CODE = kHTTPStatusCodeNotFound;
22
22
  static NSString *const FB_NO_SUCH_ELEMENT_MSG = @"An element could not be located on the page using the given search parameters";
23
23
 
24
+ static NSString *const FB_NO_SUCH_WINDOW_ERROR = @"no such window";
25
+ static const HTTPStatusCode FB_NO_SUCH_WINDOW_ERROR_CODE = kHTTPStatusCodeNotFound;
26
+ static NSString *const FB_NO_SUCH_WINDOW_MSG = @"A window could not be found. It might have been closed";
27
+
24
28
  static NSString *const FB_INVALID_ELEMENT_STATE_ERROR = @"invalid element state";
25
29
  static const HTTPStatusCode FB_INVALID_ELEMENT_STATE_ERROR_CODE = kHTTPStatusCodeBadRequest;
26
30
  static NSString *const FB_INVALID_ELEMENT_STATE_MSG = @"An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)";
@@ -155,6 +159,15 @@ static NSString *const FB_NO_SUCH_DRIVER_MSG = @"A session is either terminated
155
159
  traceback:traceback];
156
160
  }
157
161
 
162
+ + (instancetype)noSuchWindowErrorWithMessage:(NSString *)message
163
+ traceback:(NSString *)traceback
164
+ {
165
+ return [[FBCommandStatus alloc] initWithError:FB_NO_SUCH_WINDOW_ERROR
166
+ statusCode:FB_NO_SUCH_WINDOW_ERROR_CODE
167
+ message:message ?: FB_NO_SUCH_WINDOW_MSG
168
+ traceback:traceback];
169
+ }
170
+
158
171
  + (instancetype)invalidElementStateErrorWithMessage:(NSString *)message
159
172
  traceback:(NSString *)traceback
160
173
  {