blob: 0c5b3c4750acd95561d73952f429091a4c47d2f0 [file] [log] [blame]
// Copyright 2017 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.
#include <vector>
#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#include "base/macros.h"
#include "base/strings/stringprintf.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/web_state/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/web/web_state/context_menu_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// Unit tests for ios/web/web_state/js/resources/context_menu.js.
namespace {
// Test coordinates and expected result for __gCrWeb.getElementFromPoint call.
struct TestCoordinatesAndExpectedValue {
TestCoordinatesAndExpectedValue(CGFloat x, CGFloat y, id expected_value)
: x(x), y(y), expected_value(expected_value) {}
CGFloat x = 0;
CGFloat y = 0;
id expected_value = nil;
};
} // namespace
namespace web {
// Test fixture to test context_menu.js.
class ContextMenuJsTest : public web::WebTestWithWebState {
protected:
// Verifies that __gCrWeb.getElementFromPoint returns |expected_value| for
// the given image |html|.
void ImageTesterHelper(NSString* html, NSDictionary* expected_value) {
NSString* page_content_template =
@"<html><body style='margin-left:10px;margin-top:10px;'>"
"<div style='width:100px;height:100px;'>"
" <p style='position:absolute;left:25px;top:25px;"
" width:50px;height:50px'>"
"%@"
" Chrome rocks!"
" </p></div></body></html>";
NSString* page_content =
[NSString stringWithFormat:page_content_template, html];
TestCoordinatesAndExpectedValue test_data[] = {
// Point outside the document margins.
{0, 0, @{}},
// Point inside the <p> element.
{20, 20, expected_value},
// Point outside the <p> element.
{GetWebViewContentSize().width / 2, 50, @{}},
};
for (size_t i = 0; i < arraysize(test_data); i++) {
const TestCoordinatesAndExpectedValue& data = test_data[i];
LoadHtml(page_content);
id result = ExecuteGetElementFromPointJavaScript(data.x, data.y);
EXPECT_NSEQ(data.expected_value, result)
<< " in test " << i << ": (" << data.x << ", " << data.y << ")";
}
}
// Returns web view's content size from the current web state.
CGSize GetWebViewContentSize() {
return web_state()->GetWebViewProxy().scrollViewProxy.contentSize;
}
// Executes __gCrWeb.getElementFromPoint script and syncronously returns the
// result. |x| and |y| are points in web view coordinate system.
id ExecuteGetElementFromPointJavaScript(CGFloat x, CGFloat y) {
CGSize contentSize = GetWebViewContentSize();
NSString* const script = [NSString
stringWithFormat:@"__gCrWeb.getElementFromPoint(%g, %g, %g, %g)", x, y,
contentSize.width, contentSize.height];
return ExecuteJavaScript(script);
}
};
// Tests that __gCrWeb.getElementFromPoint function returns correct src.
TEST_F(ContextMenuJsTest, GetImageUrlAtPoint) {
NSString* html =
@"<img id='foo' style='width:200;height:200;' src='file:///bogus'/>";
NSDictionary* expected_value = @{
kContextMenuElementSource : @"file:///bogus",
kContextMenuElementReferrerPolicy : @"default",
};
ImageTesterHelper(html, expected_value);
}
// Tests that __gCrWeb.getElementFromPoint function returns correct title.
TEST_F(ContextMenuJsTest, GetImageTitleAtPoint) {
NSString* html = @"<img id='foo' title='Hello world!'"
"style='width:200;height:200;' src='file:///bogus'/>";
NSDictionary* expected_value = @{
kContextMenuElementSource : @"file:///bogus",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementTitle : @"Hello world!",
};
ImageTesterHelper(html, expected_value);
}
// Tests that __gCrWeb.getElementFromPoint function returns correct href.
TEST_F(ContextMenuJsTest, GetLinkImageUrlAtPoint) {
NSString* html =
@"<a href='file:///linky'>"
"<img id='foo' style='width:200;height:200;' src='file:///bogus'/>"
"</a>";
NSDictionary* expected_value = @{
kContextMenuElementSource : @"file:///bogus",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"file:///linky",
};
ImageTesterHelper(html, expected_value);
}
TEST_F(ContextMenuJsTest, TextAreaStopsProximity) {
NSString* html =
@"<html><body style='margin-left:10px;margin-top:10px;'>"
"<div style='width:100px;height:100px;'>"
"<img id='foo'"
" style='position:absolute;left:0px;top:0px;width:50px;height:50px'"
" src='file:///bogus' />"
"<input type='text' name='name'"
" style='position:absolute;left:5px;top:5px; "
"width:40px;height:40px'/>"
"</div></body> </html>";
NSDictionary* success = @{
kContextMenuElementSource : @"file:///bogus",
kContextMenuElementReferrerPolicy : @"default",
};
NSDictionary* failure = @{};
TestCoordinatesAndExpectedValue test_data[] = {
{2, 20, success}, {10, 10, failure},
};
for (size_t i = 0; i < arraysize(test_data); i++) {
const TestCoordinatesAndExpectedValue& data = test_data[i];
LoadHtml(html);
id result = ExecuteGetElementFromPointJavaScript(data.x, data.y);
EXPECT_NSEQ(data.expected_value, result)
<< " in test " << i << ": (" << data.x << ", " << data.y << ")";
}
}
// Tests the javascript of the url of the an image present in the DOM.
TEST_F(ContextMenuJsTest, LinkOfImage) {
// A page with a large image surrounded by a link.
static const char image[] =
"<a href='%s'><img width=400 height=400 src='foo'></img></a>";
// A page with a link to a destination URL.
LoadHtml(base::StringPrintf(image, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('img')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(20, 20);
NSDictionary* expected_result = @{
kContextMenuElementSource :
[NSString stringWithFormat:@"%sfoo", BaseUrl().c_str()],
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"http://destination/",
};
EXPECT_NSEQ(expected_result, result);
// A page with a link with some JavaScript that does not result in a NOP.
LoadHtml(base::StringPrintf(image, "javascript:console.log('whatever')"));
result = ExecuteGetElementFromPointJavaScript(20, 20);
expected_result = @{
kContextMenuElementSource :
[NSString stringWithFormat:@"%sfoo", BaseUrl().c_str()],
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"javascript:console.log(",
};
EXPECT_NSEQ(expected_result, result);
// A list of JavaScripts that result in a NOP.
std::vector<std::string> nop_javascripts;
nop_javascripts.push_back(";");
nop_javascripts.push_back("void(0);");
nop_javascripts.push_back("void(0); void(0); void(0)");
for (auto js : nop_javascripts) {
// A page with a link with some JavaScript that results in a NOP.
const std::string javascript = std::string("javascript:") + js;
LoadHtml(base::StringPrintf(image, javascript.c_str()));
result = ExecuteGetElementFromPointJavaScript(20, 20);
expected_result = @{
kContextMenuElementSource :
[NSString stringWithFormat:@"%sfoo", BaseUrl().c_str()],
kContextMenuElementReferrerPolicy : @"default",
};
// Make sure the returned JSON does not have an 'href' key.
EXPECT_NSEQ(expected_result, result);
}
}
// Tests context menu invoked on an image with "-webkit-touch-callout:none"
// style and parent link.
TEST_F(ContextMenuJsTest, LinkOfImageWithCalloutNone) {
// A page with an image surrounded by a link.
static const char image_html[] =
"<a href='%s'>"
"<img style='width:9;height:9;display:block;-webkit-touch-callout:none;'>"
"</a>";
// A page with a link to a destination URL.
LoadHtml(base::StringPrintf(image_html, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('img')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(5, 5);
NSDictionary* expected_result = @{
kContextMenuElementInnerText : @"",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"http://destination/",
};
EXPECT_NSEQ(expected_result, result);
}
// Tests that the GetElementFromPoint script reports "never" as the referrer
// policy for pages that have an unsupported policy in a meta tag.
TEST_F(ContextMenuJsTest, UnsupportedReferrerPolicy) {
// A page with an unsupported referrer meta tag and a 400x400 image.
static const char kInvalidReferrerTag[] =
"<meta name=\"referrer\" content=\"unsupported-value\">"
"<img width=400 height=400 src='foo'></img>";
// Load the invalid meta tag
LoadHtml(kInvalidReferrerTag);
id result = ExecuteGetElementFromPointJavaScript(20, 20);
ASSERT_TRUE([result isKindOfClass:[NSDictionary class]]);
EXPECT_NSEQ(@"never", result[kContextMenuElementReferrerPolicy]);
}
// Tests that a callout information about a link is displayed when
// -webkit-touch-callout property is not specified. Please see:
// https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout
TEST_F(ContextMenuJsTest, LinkOfTextWithoutCalloutProperty) {
const char kLinkHtml[] = "<a href='%s'>link</a>";
LoadHtml(base::StringPrintf(kLinkHtml, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('a')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(1, 1);
NSDictionary* expected_result = @{
kContextMenuElementInnerText : @"link",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"http://destination/",
};
EXPECT_NSEQ(expected_result, result);
}
// Tests that a callout information about a link is displayed when
// -webkit-touch-callout property is set to default. Please see:
// https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout
TEST_F(ContextMenuJsTest, LinkOfTextWithCalloutDefault) {
const char kLinkHtml[] =
"<a href='%s' style='-webkit-touch-callout:default;'>link</a>";
LoadHtml(base::StringPrintf(kLinkHtml, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('a')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(1, 1);
NSDictionary* expected_result = @{
kContextMenuElementInnerText : @"link",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"http://destination/",
};
EXPECT_NSEQ(expected_result, result);
}
// Tests that no callout information about a link is displayed when
// -webkit-touch-callout property is set to none. Please see:
// https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout
TEST_F(ContextMenuJsTest, LinkOfTextWithCalloutNone) {
const char kLinkHtml[] =
"<a href='%s' style='-webkit-touch-callout:none;'>link</a>";
LoadHtml(base::StringPrintf(kLinkHtml, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('a')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(1, 1);
EXPECT_NSEQ(@{}, result);
}
// Tests that -webkit-touch-callout property can be inherited from ancester if
// it's not specified. Please see:
// https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout
TEST_F(ContextMenuJsTest, LinkOfTextWithCalloutFromAncester) {
const char kLinkHtml[] =
"<body style='-webkit-touch-callout: none'>"
" <a href='%s'>link</a>"
"</body>";
LoadHtml(base::StringPrintf(kLinkHtml, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('a')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(1, 1);
EXPECT_NSEQ(@{}, result);
}
// Tests that setting -webkit-touch-callout property can override the value
// inherited from ancester. Please see:
// https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout
TEST_F(ContextMenuJsTest, LinkOfTextWithCalloutOverride) {
const char kLinkHtml[] =
"<body style='-webkit-touch-callout: none'>"
" <a href='%s' style='-webkit-touch-callout: default'>link</a>"
"</body>";
LoadHtml(base::StringPrintf(kLinkHtml, "http://destination"));
ExecuteJavaScript(@"document.getElementsByTagName('a')"); // Force layout.
id result = ExecuteGetElementFromPointJavaScript(1, 1);
NSDictionary* expected_result = @{
kContextMenuElementInnerText : @"link",
kContextMenuElementReferrerPolicy : @"default",
kContextMenuElementHyperlink : @"http://destination/",
};
EXPECT_NSEQ(expected_result, result);
}
} // namespace web