blob: c5bc647557e462829c3a5032f0abd2296aa7f6bd [file] [log] [blame]
// Copyright 2014 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/js_messaging/web_view_js_utils.h"
#import <WebKit/WebKit.h>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/ios/ios_util.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#import "ios/web/test/fakes/crw_fake_script_message_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "testing/platform_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
namespace web {
using WebViewJsUtilsTest = PlatformTest;
// Tests that ValueResultFromWKResult converts nil value to nullptr.
TEST_F(WebViewJsUtilsTest, ValueResultFromUndefinedWKResult) {
EXPECT_FALSE(ValueResultFromWKResult(nil));
}
// Tests that ValueResultFromWKResult converts string to Value::Type::STRING.
TEST_F(WebViewJsUtilsTest, ValueResultFromStringWKResult) {
std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(@"test"));
EXPECT_TRUE(value);
EXPECT_EQ(base::Value::Type::STRING, value->type());
std::string converted_result;
value->GetAsString(&converted_result);
EXPECT_EQ("test", converted_result);
}
// Tests that ValueResultFromWKResult converts inetger to Value::Type::DOUBLE.
// NOTE: WKWebView API returns all numbers as kCFNumberFloat64Type, so there is
// no way to tell if the result is integer or double.
TEST_F(WebViewJsUtilsTest, ValueResultFromIntegerWKResult) {
std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(@1));
EXPECT_TRUE(value);
EXPECT_EQ(base::Value::Type::DOUBLE, value->type());
double converted_result = 0;
value->GetAsDouble(&converted_result);
EXPECT_EQ(1, converted_result);
}
// Tests that ValueResultFromWKResult converts double to Value::Type::DOUBLE.
TEST_F(WebViewJsUtilsTest, ValueResultFromDoubleWKResult) {
std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(@3.14));
EXPECT_TRUE(value);
EXPECT_EQ(base::Value::Type::DOUBLE, value->type());
double converted_result = 0;
value->GetAsDouble(&converted_result);
EXPECT_EQ(3.14, converted_result);
}
// Tests that ValueResultFromWKResult converts bool to Value::Type::BOOLEAN.
TEST_F(WebViewJsUtilsTest, ValueResultFromBoolWKResult) {
std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(@YES));
EXPECT_TRUE(value);
EXPECT_EQ(base::Value::Type::BOOLEAN, value->type());
bool converted_result = false;
value->GetAsBoolean(&converted_result);
EXPECT_TRUE(converted_result);
}
// Tests that ValueResultFromWKResult converts null to Value::Type::NONE.
TEST_F(WebViewJsUtilsTest, ValueResultFromNullWKResult) {
std::unique_ptr<base::Value> value(
web::ValueResultFromWKResult([NSNull null]));
EXPECT_TRUE(value);
EXPECT_EQ(base::Value::Type::NONE, value->type());
}
// Tests that ValueResultFromWKResult converts NSDictionaries to properly
// initialized base::DictionaryValue.
TEST_F(WebViewJsUtilsTest, ValueResultFromDictionaryWKResult) {
NSDictionary* test_dictionary =
@{@"Key1" : @"Value1",
@"Key2" : @{@"Key3" : @42}};
std::unique_ptr<base::Value> value(
web::ValueResultFromWKResult(test_dictionary));
base::DictionaryValue* dictionary = nullptr;
value->GetAsDictionary(&dictionary);
EXPECT_NE(nullptr, dictionary);
std::string value1;
dictionary->GetString("Key1", &value1);
EXPECT_EQ("Value1", value1);
base::DictionaryValue const* inner_dictionary = nullptr;
dictionary->GetDictionary("Key2", &inner_dictionary);
EXPECT_NE(nullptr, inner_dictionary);
double value3;
inner_dictionary->GetDouble("Key3", &value3);
EXPECT_EQ(42, value3);
}
// Tests that ValueResultFromWKResult converts NSArray to properly
// initialized base::ListValue.
TEST_F(WebViewJsUtilsTest, ValueResultFromArrayWKResult) {
NSArray* test_array = @[ @"Value1", @[ @YES ], @42 ];
std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(test_array));
base::ListValue* list = nullptr;
value->GetAsList(&list);
EXPECT_NE(nullptr, list);
size_t list_size = 3;
EXPECT_EQ(list_size, list->GetSize());
std::string value1;
list->GetString(0, &value1);
EXPECT_EQ("Value1", value1);
base::ListValue const* inner_list = nullptr;
list->GetList(1, &inner_list);
EXPECT_NE(nullptr, inner_list);
double value3;
list->GetDouble(2, &value3);
EXPECT_EQ(42, value3);
}
// Tests that an NSDictionary with a cycle does not cause infinite recursion.
TEST_F(WebViewJsUtilsTest, ValueResultFromDictionaryWithDepthCheckWKResult) {
// Create a dictionary with a cycle.
NSMutableDictionary* test_dictionary =
[NSMutableDictionary dictionaryWithCapacity:1];
NSMutableDictionary* test_dictionary_2 =
[NSMutableDictionary dictionaryWithCapacity:1];
const char* key = "key";
NSString* obj_c_key = [NSString stringWithCString:key
encoding:NSASCIIStringEncoding];
test_dictionary[obj_c_key] = test_dictionary_2;
test_dictionary_2[obj_c_key] = test_dictionary;
// Break the retain cycle so that the dictionaries are freed.
base::ScopedClosureRunner runner(base::BindOnce(^{
[test_dictionary_2 removeAllObjects];
}));
// Check that parsing the dictionary stopped at a depth of
// |kMaximumParsingRecursionDepth|.
std::unique_ptr<base::Value> value =
web::ValueResultFromWKResult(test_dictionary);
base::DictionaryValue* current_dictionary = nullptr;
base::DictionaryValue* inner_dictionary = nullptr;
value->GetAsDictionary(&current_dictionary);
EXPECT_NE(nullptr, current_dictionary);
for (int current_depth = 0; current_depth <= kMaximumParsingRecursionDepth;
current_depth++) {
EXPECT_NE(nullptr, current_dictionary);
inner_dictionary = nullptr;
current_dictionary->GetDictionary(key, &inner_dictionary);
current_dictionary = inner_dictionary;
}
EXPECT_EQ(nullptr, current_dictionary);
}
// Tests that an NSArray with a cycle does not cause infinite recursion.
TEST_F(WebViewJsUtilsTest, ValueResultFromArrayWithDepthCheckWKResult) {
// Create an array with a cycle.
NSMutableArray* test_array = [NSMutableArray arrayWithCapacity:1];
NSMutableArray* test_array_2 = [NSMutableArray arrayWithCapacity:1];
test_array[0] = test_array_2;
test_array_2[0] = test_array;
// Break the retain cycle so that the arrays are freed.
base::ScopedClosureRunner runner(base::BindOnce(^{
[test_array removeAllObjects];
}));
// Check that parsing the array stopped at a depth of
// |kMaximumParsingRecursionDepth|.
std::unique_ptr<base::Value> value = web::ValueResultFromWKResult(test_array);
base::ListValue* current_list = nullptr;
base::ListValue* inner_list = nullptr;
value->GetAsList(&current_list);
EXPECT_NE(nullptr, current_list);
for (int current_depth = 0; current_depth <= kMaximumParsingRecursionDepth;
current_depth++) {
EXPECT_NE(nullptr, current_list);
inner_list = nullptr;
current_list->GetList(0, &inner_list);
current_list = inner_list;
}
EXPECT_EQ(nullptr, current_list);
}
// Tests that ExecuteJavaScript returns an error if there is no web view.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptNoWebView) {
__block bool complete = false;
__block id block_result = nil;
__block NSError* block_error = nil;
web::ExecuteJavaScript(nil, @"return true;", ^(id result, NSError* error) {
block_result = [result copy];
block_error = [error copy];
complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return complete;
}));
EXPECT_TRUE(block_error);
EXPECT_FALSE(block_result);
}
// Tests that javascript can be executed.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScript) {
WKWebView* web_view = [[WKWebView alloc] init];
__block bool complete = false;
__block id block_result = nil;
__block NSError* block_error = nil;
web::ExecuteJavaScript(web_view, @"true", ^(id result, NSError* error) {
block_result = [result copy];
block_error = [error copy];
complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return complete;
}));
EXPECT_FALSE(block_error);
EXPECT_TRUE(block_result);
}
// Tests that javascript can be executed in the page content world when no
// content world or web frame are specified.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptPageContentWorldByDefault) {
if (!base::ios::IsRunningOnIOS14OrLater()) {
return;
}
__block bool complete = false;
__block id result = nil;
__block NSError* error = nil;
WKWebView* web_view = [[WKWebView alloc] init];
if (@available(iOS 14, *)) {
__block bool set_value_complete = false;
__block NSError* set_value_error = nil;
// Set |value| in the page content world.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld,
/*frame_info=*/nil, @"var value = 3;",
^(id result, NSError* error) {
set_value_error = [error copy];
set_value_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return set_value_complete;
}));
ASSERT_FALSE(set_value_error);
// Retrieve the value without specifying the content world to verify that
// ExecuteJavaScript defaults to the page content world.
web::ExecuteJavaScript(web_view, /*content_world=*/nil, /*frame_info=*/nil,
@"value", ^(id block_result, NSError* block_error) {
result = [block_result copy];
error = [block_error copy];
complete = true;
});
}
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return complete;
}));
EXPECT_FALSE(error);
EXPECT_TRUE(result);
EXPECT_NSEQ(@(3), result);
}
// Tests that javascript can be executed when the page content world is
// explicitly specified but no frame info is given, implying the main frame.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptInPageWorldWithoutFrameInfo) {
if (!base::ios::IsRunningOnIOS14OrLater()) {
return;
}
__block bool complete = false;
__block id result = nil;
__block NSError* error = nil;
WKWebView* web_view = [[WKWebView alloc] init];
if (@available(iOS 14, *)) {
__block bool set_value_complete = false;
__block NSError* set_value_error = nil;
// Set |value| in the page content world.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld,
/*frame_info=*/nil, @"var value = 3;",
^(id result, NSError* error) {
set_value_error = [error copy];
set_value_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return set_value_complete;
}));
ASSERT_FALSE(set_value_error);
// Ensure the value can be accessed when specifying the content world.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld,
/*frame_info=*/nil, @"value",
^(id block_result, NSError* block_error) {
result = [block_result copy];
error = [block_error copy];
complete = true;
});
}
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return complete;
}));
EXPECT_FALSE(error);
EXPECT_TRUE(result);
EXPECT_NSEQ(@(3), result);
}
// Tests that javascript can be executed in the page content world when the page
// content world and web frame are both specified.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptPageContentWorld) {
if (!base::ios::IsRunningOnIOS14OrLater()) {
return;
}
CRWFakeScriptMessageHandler* script_message_handler =
[[CRWFakeScriptMessageHandler alloc] init];
WKWebView* web_view = [[WKWebView alloc] init];
[web_view.configuration.userContentController
addScriptMessageHandler:script_message_handler
name:@"TestHandler"];
// Fetch WKFrameInfo instance.
web::ExecuteJavaScript(
web_view,
@"window.webkit.messageHandlers['TestHandler'].postMessage({});",
/*completion_handler=*/nil);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return !!script_message_handler.lastReceivedScriptMessage.frameInfo;
}));
WKFrameInfo* frame_info =
script_message_handler.lastReceivedScriptMessage.frameInfo;
__block bool complete = false;
__block id result = nil;
__block NSError* error = nil;
if (@available(iOS 14, *)) {
__block bool set_value_complete = false;
__block NSError* set_value_error = nil;
// Set |value| in the page content world.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
@"var value = 3;", ^(id result, NSError* error) {
set_value_error = [error copy];
set_value_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return set_value_complete;
}));
ASSERT_FALSE(set_value_error);
// Ensure the value can be accessed when specifying |frame_info|.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
@"value", ^(id block_result, NSError* block_error) {
result = [block_result copy];
error = [block_error copy];
complete = true;
});
}
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return complete;
}));
EXPECT_FALSE(error);
EXPECT_TRUE(result);
EXPECT_NSEQ(@(3), result);
}
// Tests that javascript can be executed in an isolated content world and that
// it can not be accessed from the page content world.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptIsolatedWorld) {
if (!base::ios::IsRunningOnIOS14OrLater()) {
return;
}
CRWFakeScriptMessageHandler* script_message_handler =
[[CRWFakeScriptMessageHandler alloc] init];
WKWebView* web_view = [[WKWebView alloc] init];
[web_view.configuration.userContentController
addScriptMessageHandler:script_message_handler
name:@"TestHandler"];
// Fetch WKFrameInfo instance.
web::ExecuteJavaScript(
web_view,
@"window.webkit.messageHandlers['TestHandler'].postMessage({});",
/*completion_handler=*/nil);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return !!script_message_handler.lastReceivedScriptMessage.frameInfo;
}));
WKFrameInfo* frame_info =
script_message_handler.lastReceivedScriptMessage.frameInfo;
if (@available(iOS 14, *)) {
__block bool set_value_complete = false;
__block NSError* set_value_error = nil;
// Set |value| in the page content world.
web::ExecuteJavaScript(web_view, WKContentWorld.defaultClientWorld,
frame_info, @"var value = 3;",
^(id result, NSError* error) {
set_value_error = [error copy];
set_value_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return set_value_complete;
}));
ASSERT_FALSE(set_value_error);
__block bool isolated_world_complete = false;
__block id isolated_world_result = nil;
__block NSError* isolated_world_error = nil;
// Ensure the value can be accessed when specifying an isolated world and
// |frame_info|.
web::ExecuteJavaScript(web_view, WKContentWorld.defaultClientWorld,
frame_info, @"value",
^(id block_result, NSError* block_error) {
isolated_world_result = [block_result copy];
isolated_world_error = [block_error copy];
isolated_world_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return isolated_world_complete;
}));
EXPECT_FALSE(isolated_world_error);
EXPECT_TRUE(isolated_world_result);
EXPECT_NSEQ(@(3), isolated_world_result);
__block bool page_world_complete = false;
__block id page_world_result = nil;
__block NSError* page_world_error = nil;
// The value should not be accessible from the page content world.
web::ExecuteJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
@"try { value } catch (error) { false }",
^(id block_result, NSError* block_error) {
page_world_result = [block_result copy];
page_world_error = [block_error copy];
page_world_complete = true;
});
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return page_world_complete;
}));
EXPECT_FALSE(page_world_error);
EXPECT_TRUE(page_world_result);
EXPECT_FALSE([page_world_result boolValue]);
}
}
} // namespace web