Skip to content

Commit 6a9b497

Browse files
Mykola Mokhnachfacebook-github-bot
authored andcommitted
Add W3C element screenshot endpoint support
Summary: Sometimes it is useful to get a screenshot of the particular element. This is especially useful for non-portrait mode, where things natively available from XCTest are kind of broken. Unfortunately, this endpoint only works since Xcode9 SDK and requires `screenshotDataForQuality` selector to be available. Closes facebookarchive/WebDriverAgent#815 Differential Revision: D6543505 Pulled By: marekcirkos fbshipit-source-id: 08dc4a556c34e2485c478ab5acb344daef5e73b0
1 parent 14085ea commit 6a9b497

File tree

7 files changed

+124
-2
lines changed

7 files changed

+124
-2
lines changed

WebDriverAgent.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; };
1111
711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; };
1212
7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */; };
13+
7126FF0E1FD99C7E00DEFB38 /* FBElementScreenshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7126FF0D1FD99C7E00DEFB38 /* FBElementScreenshotTests.m */; };
1314
712A0C851DA3E459007D02E5 /* FBXPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 712A0C841DA3E459007D02E5 /* FBXPathTests.m */; };
1415
712A0C871DA3E55D007D02E5 /* FBXPath-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */; };
1516
712A0C8C1DA3F25B007D02E5 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7174AF031D9D39AF008C8AD5 /* libxml2.tbd */; };
@@ -401,6 +402,7 @@
401402
711084421DA3AA7500F913D6 /* FBXPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXPath.h; sourceTree = "<group>"; };
402403
711084431DA3AA7500F913D6 /* FBXPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPath.m; sourceTree = "<group>"; };
403404
7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBPickerWheelSelectTests.m; sourceTree = "<group>"; };
405+
7126FF0D1FD99C7E00DEFB38 /* FBElementScreenshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementScreenshotTests.m; sourceTree = "<group>"; };
404406
712A0C841DA3E459007D02E5 /* FBXPathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPathTests.m; sourceTree = "<group>"; };
405407
712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBXPath-Private.h"; sourceTree = "<group>"; };
406408
7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBPickerWheel.h"; sourceTree = "<group>"; };
@@ -1058,6 +1060,7 @@
10581060
children = (
10591061
EE9B76991CF799F400275851 /* FBAlertTests.m */,
10601062
EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */,
1063+
7126FF0D1FD99C7E00DEFB38 /* FBElementScreenshotTests.m */,
10611064
EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */,
10621065
EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */,
10631066
EE1E06DB1D18090F007CF043 /* FBIntegrationTestCase.h */,
@@ -1801,6 +1804,7 @@
18011804
files = (
18021805
EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */,
18031806
EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */,
1807+
7126FF0E1FD99C7E00DEFB38 /* FBElementScreenshotTests.m in Sources */,
18041808
);
18051809
runOnlyForDeploymentPostprocessing = 0;
18061810
};

WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import <arpa/inet.h>
1313
#import <ifaddrs.h>
1414
#include <notify.h>
15+
#import <objc/runtime.h>
1516

1617
#import "FBSpringboardApplication.h"
1718
#import "FBErrorBuilder.h"
@@ -43,7 +44,7 @@ - (BOOL)fb_goToHomescreenWithError:(NSError **)error
4344

4445
- (NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error
4546
{
46-
Class xcScreenClass = NSClassFromString(@"XCUIScreen");
47+
Class xcScreenClass = objc_lookUpClass("XCUIScreen");
4748
if (nil == xcScreenClass) {
4849
NSData *result = [[XCAXClient_iOS sharedClient] screenshotData];
4950
if (nil == result) {

WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ NS_ASSUME_NONNULL_BEGIN
5959
*/
6060
- (BOOL)fb_waitUntilSnapshotIsStable;
6161

62+
/**
63+
Returns screenshot of the particular element
64+
@param error If there is an error, upon return contains an NSError object that describes the problem.
65+
@return Element screenshot as PNG-encoded data or nil in case of failure
66+
*/
67+
- (nullable NSData *)fb_screenshotWithError:(NSError **)error;
68+
6269
@end
6370

6471
NS_ASSUME_NONNULL_END

WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#import "XCUIElement+FBUtilities.h"
1111

12+
#import <objc/runtime.h>
13+
1214
#import "FBAlert.h"
1315
#import "FBLogger.h"
1416
#import "FBMacros.h"
@@ -18,6 +20,7 @@
1820
#import "FBXCodeCompatibility.h"
1921
#import "XCAXClient_iOS.h"
2022
#import "XCUIElement+FBWebDriverAttributes.h"
23+
#import "XCUIScreen.h"
2124

2225
@implementation XCUIElement (FBUtilities)
2326

@@ -124,4 +127,48 @@ - (BOOL)fb_waitUntilSnapshotIsStable
124127
return result;
125128
}
126129

130+
- (NSData *)fb_screenshotWithError:(NSError **)error
131+
{
132+
if (CGRectIsEmpty(self.frame)) {
133+
if (error) {
134+
*error = [[FBErrorBuilder.builder withDescription:@"Cannot get a screenshot of zero-sized element"] build];
135+
}
136+
return nil;
137+
}
138+
139+
Class xcScreenClass = objc_lookUpClass("XCUIScreen");
140+
if (nil == xcScreenClass) {
141+
if (error) {
142+
*error = [[FBErrorBuilder.builder withDescription:@"Element screenshots are only available since Xcode9 SDK"] build];
143+
}
144+
return nil;
145+
}
146+
147+
XCUIScreen *mainScreen = (XCUIScreen *)[xcScreenClass mainScreen];
148+
NSData *result = [mainScreen screenshotDataForQuality:1 rect:self.frame error:error];
149+
if (nil == result) {
150+
return nil;
151+
}
152+
153+
UIImage *image = [UIImage imageWithData:result];
154+
UIInterfaceOrientation orientation = self.application.interfaceOrientation;
155+
UIImageOrientation imageOrientation = UIImageOrientationUp;
156+
// The received element screenshot will be rotated, if the current interface orientation differs from portrait, so we need to fix that first
157+
if (orientation == UIInterfaceOrientationLandscapeRight) {
158+
imageOrientation = UIImageOrientationLeft;
159+
} else if (orientation == UIInterfaceOrientationLandscapeLeft) {
160+
imageOrientation = UIImageOrientationRight;
161+
} else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
162+
imageOrientation = UIImageOrientationDown;
163+
}
164+
CGSize size = image.size;
165+
UIGraphicsBeginImageContext(CGSizeMake(size.width, size.height));
166+
[[UIImage imageWithCGImage:(CGImageRef)[image CGImage] scale:1.0 orientation:imageOrientation] drawInRect:CGRectMake(0, 0, size.width, size.height)];
167+
UIImage *fixedImage = UIGraphicsGetImageFromCurrentImageContext();
168+
UIGraphicsEndImageContext();
169+
170+
// The resulting data is a JPEG image, so we need to convert it to PNG representation
171+
return (NSData *)UIImagePNGRepresentation(fixedImage);
172+
}
173+
127174
@end

WebDriverAgentLib/Commands/FBElementCommands.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ + (NSArray *)routes
5757
[[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)],
5858
[[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)],
5959
[[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)],
60+
[[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
6061
[[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)],
6162
[[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)],
6263
[[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
@@ -381,6 +382,19 @@ + (NSArray *)routes
381382
});
382383
}
383384

385+
+ (id<FBResponsePayload>)handleElementScreenshot:(FBRouteRequest *)request
386+
{
387+
FBElementCache *elementCache = request.session.elementCache;
388+
XCUIElement *element = [elementCache elementForUUID:request.parameters[@"uuid"]];
389+
NSError *error;
390+
NSData *screenshotData = [element fb_screenshotWithError:&error];
391+
if (nil == screenshotData) {
392+
return FBResponseWithError(error);
393+
}
394+
NSString *screenshot = [screenshotData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
395+
return FBResponseWithObject(screenshot);
396+
}
397+
384398
static const CGFloat DEFAULT_OFFSET = (CGFloat)0.2;
385399

386400
+ (id<FBResponsePayload>)handleWheelSelect:(FBRouteRequest *)request

WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#import "XCTestPrivateSymbols.h"
1111

12+
#import <objc/runtime.h>
13+
1214
#import "FBRuntimeUtils.h"
1315

1416
NSNumber *FB_XCAXAIsVisibleAttribute;
@@ -38,7 +40,7 @@
3840

3941
void *FBRetrieveXCTestSymbol(const char *name)
4042
{
41-
Class XCTestClass = NSClassFromString(@"XCTestCase");
43+
Class XCTestClass = objc_lookUpClass("XCTestCase");
4244
NSCAssert(XCTestClass != nil, @"XCTest should be already linked", XCTestClass);
4345
NSString *XCTestBinary = [NSBundle bundleForClass:XCTestClass].executablePath;
4446
const char *binaryPath = XCTestBinary.UTF8String;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
#import <XCTest/XCTest.h>
11+
12+
#import "FBIntegrationTestCase.h"
13+
#import "XCUIDevice+FBRotation.h"
14+
#import "XCUIElement+FBUtilities.h"
15+
16+
@interface FBElementScreenshotTests : FBIntegrationTestCase
17+
@end
18+
19+
@implementation FBElementScreenshotTests
20+
21+
- (void)setUp
22+
{
23+
[super setUp];
24+
static dispatch_once_t onceToken;
25+
dispatch_once(&onceToken, ^{
26+
[self launchApplication];
27+
[self goToAlertsPage];
28+
});
29+
}
30+
31+
- (void)testElementScreenshot
32+
{
33+
[[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft];
34+
XCUIElement *button = self.testedApplication.buttons[FBShowAlertButtonName];
35+
NSError *error = nil;
36+
NSData *screenshotData = [button fb_screenshotWithError:&error];
37+
if (nil == screenshotData && [error.description containsString:@"available since Xcode9"]) {
38+
return;
39+
}
40+
XCTAssertNotNil(screenshotData);
41+
XCTAssertNil(error);
42+
UIImage *image = [UIImage imageWithData:screenshotData];
43+
XCTAssertNotNil(image);
44+
XCTAssertTrue(image.size.width > image.size.height);
45+
}
46+
47+
@end

0 commit comments

Comments
 (0)