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.
- package/CHANGELOG.md +6 -0
- package/WebDriverAgentMac/IntegrationTests/AMWindowTests.m +427 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMWindowCommands.m +63 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +2 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.h +3 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.m +13 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +4 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +23 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +18 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +43 -28
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.h +111 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.m +780 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.h +61 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.m +108 -0
- package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +20 -0
- package/build/lib/commands/native-record-screen.d.ts +2 -0
- package/build/lib/commands/native-record-screen.d.ts.map +1 -1
- package/build/lib/commands/native-record-screen.js.map +1 -1
- package/build/lib/commands/windows.d.ts +22 -0
- package/build/lib/commands/windows.d.ts.map +1 -0
- package/build/lib/commands/windows.js +32 -0
- package/build/lib/commands/windows.js.map +1 -0
- package/build/lib/driver.d.ts +12 -0
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +3 -0
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +9 -0
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +9 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/build/lib/types.d.ts +26 -0
- package/build/lib/types.d.ts.map +1 -1
- package/lib/commands/native-record-screen.ts +2 -0
- package/lib/commands/windows.ts +44 -0
- package/lib/driver.ts +4 -0
- package/lib/execute-method-map.ts +9 -0
- package/lib/types.ts +28 -0
- package/node_modules/bare-url/README.md +4 -0
- package/node_modules/bare-url/index.d.ts +3 -0
- package/node_modules/bare-url/index.js +42 -18
- package/node_modules/bare-url/lib/errors.js +4 -0
- package/node_modules/bare-url/package.json +1 -1
- package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
- package/package.json +3 -2
- package/scripts/setup-accessibility.mjs +175 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [4.4.0](https://github.com/appium/appium-mac2-driver/compare/v4.3.6...v4.4.0) (2026-10-11)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -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);
|
|
@@ -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
|
{
|
|
@@ -24,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
24
24
|
@property (readonly, nonatomic) BOOL isMain;
|
|
25
25
|
/** The integer identifier of a screen */
|
|
26
26
|
@property (readonly, nonatomic) long long identifier;
|
|
27
|
+
/** The display bounds in the global coordinate space with the top-left origin (same as XCUIElement frames) */
|
|
28
|
+
@property (readonly, nonatomic) CGRect bounds;
|
|
29
|
+
/** Same as bounds, but without the menu bar and the Dock */
|
|
30
|
+
@property (readonly, nonatomic) CGRect visibleBounds;
|
|
27
31
|
|
|
28
32
|
@end
|
|
29
33
|
|
|
@@ -18,6 +18,29 @@
|
|
|
18
18
|
|
|
19
19
|
@implementation AMScreenProperties
|
|
20
20
|
|
|
21
|
+
- (CGRect)bounds
|
|
22
|
+
{
|
|
23
|
+
return CGDisplayBounds((CGDirectDisplayID)self.identifier);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
- (CGRect)visibleBounds
|
|
27
|
+
{
|
|
28
|
+
CGRect bounds = self.bounds;
|
|
29
|
+
for (NSScreen *screen in NSScreen.screens) {
|
|
30
|
+
if ([screen.deviceDescription[@"NSScreenNumber"] longLongValue] != self.identifier) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// NSScreen uses the bottom-left origin of the primary screen, which is the first one
|
|
34
|
+
CGFloat primaryHeight = NSScreen.screens.firstObject.frame.size.height;
|
|
35
|
+
CGRect visible = screen.visibleFrame;
|
|
36
|
+
return CGRectMake(visible.origin.x,
|
|
37
|
+
primaryHeight - CGRectGetMaxY(visible),
|
|
38
|
+
visible.size.width,
|
|
39
|
+
visible.size.height);
|
|
40
|
+
}
|
|
41
|
+
return bounds;
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
- (instancetype)initWithIdentifier:(long long)identifier
|
|
22
45
|
isMain:(BOOL)isMain
|
|
23
46
|
{
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
17
18
|
#import <XCTest/XCTest.h>
|
|
18
19
|
|
|
19
20
|
NS_ASSUME_NONNULL_BEGIN
|
|
@@ -79,6 +80,23 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
79
80
|
*/
|
|
80
81
|
+ (nullable NSString *)domIdentifierWithSnapshot:(nullable id)snapshot;
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
Creates an AX element for the given snapshot. The caller owns the result and must CFRelease it.
|
|
85
|
+
|
|
86
|
+
@param snapshot snapshot instance to create the AX element for
|
|
87
|
+
@return The AX element or NULL if this process lacks Accessibility permission or the element is unavailable
|
|
88
|
+
*/
|
|
89
|
+
+ (nullable AXUIElementRef)createAXElementWithSnapshot:(nullable id)snapshot CF_RETURNS_RETAINED;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
Creates an AX element for the XCTest accessibility element (XCAccessibilityElement).
|
|
93
|
+
The caller owns the result and must CFRelease it.
|
|
94
|
+
|
|
95
|
+
@param element XCTest accessibility element
|
|
96
|
+
@return The AX element or NULL if this process lacks Accessibility permission or the element is unavailable
|
|
97
|
+
*/
|
|
98
|
+
+ (nullable AXUIElementRef)createAXElementWithAccessibilityElement:(nullable id)element CF_RETURNS_RETAINED;
|
|
99
|
+
|
|
82
100
|
@end
|
|
83
101
|
|
|
84
102
|
NS_ASSUME_NONNULL_END
|