blob: 97c54ddda0e1e25606d599a4a89c46e77a8c3380 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/public/test/earl_grey/web_view_matchers.h"
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
#import "base/mac/bind_objc_block.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#import "ios/testing/wait_util.h"
#import "ios/web/interstitials/web_interstitial_impl.h"
#import "ios/web/public/test/earl_grey/js_test_util.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "net/base/mac/url_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using testing::kWaitForDownloadTimeout;
using testing::WaitUntilConditionOrTimeout;
// A helper delegate class that allows downloading responses with invalid
// SSL certs.
@interface TestURLSessionDelegate : NSObject<NSURLSessionDelegate>
@end
@implementation TestURLSessionDelegate
- (void)URLSession:(NSURLSession*)session
didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
NSURLCredential*))completionHandler {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust:serverTrust]);
}
@end
namespace {
// Enum describing loaded/blocked state of an image html element.
enum ImageState {
// Element was not loaded by WebState.
IMAGE_STATE_BLOCKED = 1,
// Element was fullt loaded by WebState.
IMAGE_STATE_LOADED,
};
// Script that returns document.body as a string.
char kGetDocumentBodyJavaScript[] =
"document.body ? document.body.textContent : null";
// Script that tests presence of css selector.
char kTestCssSelectorJavaScriptTemplate[] = "!!document.querySelector(\"%s\");";
// Fetches the image from |image_url|.
UIImage* LoadImage(const GURL& image_url) {
__block UIImage* image;
__block NSError* error;
TestURLSessionDelegate* session_delegate =
[[TestURLSessionDelegate alloc] init];
NSURLSessionConfiguration* session_config =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session =
[NSURLSession sessionWithConfiguration:session_config
delegate:session_delegate
delegateQueue:nil];
id completion_handler = ^(NSData* data, NSURLResponse*, NSError* task_error) {
error = task_error;
image = [[UIImage alloc] initWithData:data];
};
NSURLSessionDataTask* task =
[session dataTaskWithURL:net::NSURLWithGURL(image_url)
completionHandler:completion_handler];
[task resume];
bool image_loaded = WaitUntilConditionOrTimeout(kWaitForDownloadTimeout, ^{
return image || error;
});
GREYAssert(image_loaded, @"Failed to download image");
return image;
}
// Helper function for matching web views containing or not containing |text|,
// depending on the value of |should_contain_text|.
id<GREYMatcher> WebViewWithText(std::string text,
web::WebState* web_state,
bool should_contain_text) {
MatchesBlock matches = ^BOOL(WKWebView*) {
return WaitUntilConditionOrTimeout(testing::kWaitForUIElementTimeout, ^{
std::unique_ptr<base::Value> value =
web::test::ExecuteJavaScript(web_state, kGetDocumentBodyJavaScript);
std::string body;
if (value && value->GetAsString(&body)) {
BOOL contains_text = body.find(text) != std::string::npos;
return contains_text == should_contain_text;
}
return false;
});
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:should_contain_text ? @"web view containing "
: @"web view not containing "];
[description appendText:base::SysUTF8ToNSString(text)];
};
return grey_allOf(
WebViewInWebState(web_state),
[[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe],
nil);
}
// Matcher for WKWebView containing loaded or blocked image with |image_id|.
// Pass IMAGE_STATE_LOADED |image_state| to match fully loaded image and
// IMAGE_STATE_BLOCKED to match fully blocked image.
id<GREYMatcher> WebViewContainingImage(std::string image_id,
web::WebState* web_state,
ImageState image_state) {
std::string get_url_script =
base::StringPrintf("document.getElementById('%s').src", image_id.c_str());
std::unique_ptr<base::Value> url_as_value =
web::test::ExecuteJavaScript(web_state, get_url_script);
std::string url_as_string;
if (!url_as_value->GetAsString(&url_as_string))
return grey_nil();
UIImage* image = LoadImage(GURL(url_as_string));
if (!image)
return grey_nil();
CGSize expected_size = image.size;
MatchesBlock matches = ^BOOL(WKWebView*) {
return WaitUntilConditionOrTimeout(testing::kWaitForUIElementTimeout, ^{
NSString* const kGetElementAttributesScript = [NSString
stringWithFormat:@"var image = document.getElementById('%@');"
@"var imageHeight = image.height;"
@"var imageWidth = image.width;"
@"JSON.stringify({"
@" height:imageHeight,"
@" width:imageWidth"
@"});",
base::SysUTF8ToNSString(image_id)];
std::unique_ptr<base::Value> value = web::test::ExecuteJavaScript(
web_state, base::SysNSStringToUTF8(kGetElementAttributesScript));
std::string result;
if (value && value->GetAsString(&result)) {
NSString* evaluation_result = base::SysUTF8ToNSString(result);
NSData* image_attributes_as_data =
[evaluation_result dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary* image_attributes =
[NSJSONSerialization JSONObjectWithData:image_attributes_as_data
options:0
error:nil];
CGFloat height = [image_attributes[@"height"] floatValue];
CGFloat width = [image_attributes[@"width"] floatValue];
switch (image_state) {
case IMAGE_STATE_BLOCKED:
return height < expected_size.height && width < expected_size.width;
case IMAGE_STATE_LOADED:
return height == expected_size.height &&
width == expected_size.width;
}
}
return false;
});
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
switch (image_state) {
case IMAGE_STATE_BLOCKED:
[description appendText:@"web view blocked resource with id "];
break;
case IMAGE_STATE_LOADED:
[description appendText:@"web view loaded resource with id "];
break;
}
[description appendText:base::SysUTF8ToNSString(image_id)];
};
return grey_allOf(
WebViewInWebState(web_state),
[[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe],
nil);
}
} // namespace
namespace web {
id<GREYMatcher> WebViewInWebState(WebState* web_state) {
MatchesBlock matches = ^BOOL(UIView* view) {
return [view isKindOfClass:[WKWebView class]] &&
[view isDescendantOfView:web_state->GetView()];
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:@"web view in web state"];
};
return [[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe];
}
id<GREYMatcher> WebViewContainingText(std::string text, WebState* web_state) {
return WebViewWithText(text, web_state, true);
}
id<GREYMatcher> WebViewNotContainingText(std::string text,
WebState* web_state) {
return WebViewWithText(text, web_state, false);
}
id<GREYMatcher> WebViewContainingBlockedImage(std::string image_id,
WebState* web_state) {
return WebViewContainingImage(image_id, web_state, IMAGE_STATE_BLOCKED);
}
id<GREYMatcher> WebViewContainingLoadedImage(std::string image_id,
WebState* web_state) {
return WebViewContainingImage(image_id, web_state, IMAGE_STATE_LOADED);
}
id<GREYMatcher> WebViewCssSelector(std::string selector, WebState* web_state) {
MatchesBlock matches = ^BOOL(WKWebView*) {
std::string script = base::StringPrintf(kTestCssSelectorJavaScriptTemplate,
selector.c_str());
return WaitUntilConditionOrTimeout(testing::kWaitForUIElementTimeout, ^{
bool did_succeed = false;
std::unique_ptr<base::Value> value =
web::test::ExecuteJavaScript(web_state, script);
if (value) {
value->GetAsBoolean(&did_succeed);
}
return did_succeed;
});
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:@"web view selector "];
[description appendText:base::SysUTF8ToNSString(selector)];
};
return grey_allOf(
WebViewInWebState(web_state),
[[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe],
nil);
}
id<GREYMatcher> WebViewScrollView(WebState* web_state) {
MatchesBlock matches = ^BOOL(UIView* view) {
return [view isKindOfClass:[UIScrollView class]] &&
[view.superview isKindOfClass:[WKWebView class]] &&
[view isDescendantOfView:web_state->GetView()];
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:@"web view scroll view"];
};
return [[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe];
}
id<GREYMatcher> Interstitial(WebState* web_state) {
MatchesBlock matches = ^BOOL(WKWebView* view) {
web::WebInterstitialImpl* interstitial =
static_cast<web::WebInterstitialImpl*>(web_state->GetWebInterstitial());
return interstitial &&
[view isDescendantOfView:interstitial->GetContentView()];
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:@"interstitial displayed"];
};
return grey_allOf(
WebViewInWebState(web_state),
[[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe],
nil);
}
id<GREYMatcher> InterstitialContainingText(NSString* text,
WebState* web_state) {
MatchesBlock matches = ^BOOL(WKWebView* view) {
return WaitUntilConditionOrTimeout(testing::kWaitForUIElementTimeout, ^{
NSString* script = base::SysUTF8ToNSString(kGetDocumentBodyJavaScript);
id body = ExecuteScriptOnInterstitial(web_state, script);
return [body containsString:text] ? true : false;
});
};
DescribeToBlock describe = ^(id<GREYDescription> description) {
[description appendText:@"interstitial containing "];
[description appendText:text];
};
return grey_allOf(
Interstitial(web_state),
[[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe],
nil);
}
} // namespace web