// Copyright 2014 The Chromium Authors
// 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>

#import "base/apple/foundation_util.h"
#import "base/test/ios/wait_util.h"
#import "base/values.h"
#import "ios/web/public/test/fakes/crw_fake_script_message_handler.h"
#import "ios/web/public/test/js_test_util.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/abseil-cpp/absl/cleanup/cleanup.h"

using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

@interface CustomObject : NSObject
@end
@implementation CustomObject
@end

namespace web {

namespace {
// Mock implementation of `__gCrWeb.getExistingFrames` which counts the
// number of times the function is called.
NSString* const kMockGetExistingFramesScript =
    @"const originalGetExistingFrames = __gCrWeb.getExistingFrames;"
    @"__gCrWeb.getExistingFrames = function() {"
    @"  __gCrWeb.getExistingFramesCallCount ?  "
    @"    __gCrWeb.getExistingFramesCallCount++ : "
    @"    __gCrWeb.getExistingFramesCallCount = 1;"
    @"  originalGetExistingFrames.call();"
    @"};"
    @"true;";

// Returns the WKFrameInfo instance for the main frame of `web_view`.
WKFrameInfo* GetMainFrameWKFrameInfo(WKWebView* web_view) {
  // Setup a message handler and receive a message to obtain a WKFrameInfo
  // instance.
  CRWFakeScriptMessageHandler* script_message_handler =
      [[CRWFakeScriptMessageHandler alloc] init];
  [web_view.configuration.userContentController
      addScriptMessageHandler:script_message_handler
                         name:@"TestHandler"];
  web::ExecuteJavaScript(
      web_view,
      @"window.webkit.messageHandlers['TestHandler'].postMessage({});",
      /*completion_handler=*/nil);
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return !!script_message_handler.lastReceivedScriptMessage.frameInfo;
  }));

  return script_message_handler.lastReceivedScriptMessage.frameInfo;
}

// Returns the number of times that the mock of the getExistingFrames function
// setup by `AddUserScriptsForWorld` has been called.
int GetExistingFramesScriptCallCount(WKWebView* web_view,
                                     WKFrameInfo* frame_info,
                                     WKContentWorld* content_world) {
  __block int function_call_count = -1;
  __block bool js_execution_complete = false;
  web::ExecuteJavaScript(web_view, content_world, frame_info,
                         @"__gCrWeb.getExistingFramesCallCount ? "
                         @"__gCrWeb.getExistingFramesCallCount : 0;",
                         ^(id block_result, NSError* block_error) {
                           ASSERT_FALSE(block_error);
                           function_call_count = [block_result intValue];
                           js_execution_complete = true;
                         });
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return js_execution_complete;
  }));
  return function_call_count;
}

}  // namespace

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());
  ASSERT_TRUE(value->is_string());
  EXPECT_EQ("test", value->GetString());
}

// 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);
  ASSERT_EQ(base::Value::Type::DOUBLE, value->type());
  EXPECT_EQ(1, value->GetDouble());
}

// 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);
  ASSERT_EQ(base::Value::Type::DOUBLE, value->type());
  EXPECT_EQ(3.14, value->GetDouble());
}

// Tests that ValueResultFromWKResult converts bool to Value::Type::BOOLEAN.
TEST_F(WebViewJsUtilsTest, ValueResultFromBoolWKResult) {
  std::unique_ptr<base::Value> value(web::ValueResultFromWKResult(@YES));
  ASSERT_TRUE(value);
  ASSERT_TRUE(value->is_bool());
  EXPECT_TRUE(value->GetBool());
}

// 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::DictValue* dictionary = value->GetIfDict();
  EXPECT_NE(nullptr, dictionary);

  std::string* value1 = dictionary->FindString("Key1");
  EXPECT_EQ("Value1", *value1);

  base::DictValue const* inner_dictionary = dictionary->FindDict("Key2");
  EXPECT_NE(nullptr, inner_dictionary);

  EXPECT_EQ(42, *inner_dictionary->FindDouble("Key3"));
}

// 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));
  ASSERT_TRUE(value->is_list());
  const base::ListValue& list = value->GetList();

  size_t list_size = 3;
  ASSERT_EQ(list_size, list.size());

  ASSERT_TRUE(list[0].is_string());
  std::string value1 = list[0].GetString();
  EXPECT_EQ("Value1", value1);

  EXPECT_TRUE(list[1].is_list());

  ASSERT_TRUE(list[2].is_double());
  double value3 = list[2].GetDouble();
  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.
  absl::Cleanup cycle_breaker = ^{
    [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::DictValue* current_dictionary = value->GetIfDict();
  base::DictValue* inner_dictionary = nullptr;

  EXPECT_NE(nullptr, current_dictionary);

  for (int current_depth = 0; current_depth <= kMaximumParsingRecursionDepth;
       current_depth++) {
    EXPECT_NE(nullptr, current_dictionary);
    inner_dictionary = current_dictionary->FindDict(key);
    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.
  absl::Cleanup cycle_breaker = ^{
    [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;

  ASSERT_TRUE(value->is_list());
  current_list = &value->GetList();

  for (int current_depth = 0; current_depth <= kMaximumParsingRecursionDepth;
       current_depth++) {
    ASSERT_TRUE(current_list);

    inner_list = nullptr;
    if (!current_list->empty()) {
      inner_list = (*current_list)[0].GetIfList();
    }
    current_list = inner_list;
  }
  EXPECT_FALSE(current_list);
}

// Tests that NSObjectFromValueResult converts nullptr to nil.
TEST_F(WebViewJsUtilsTest, NSObjectFromNullptr) {
  id wk_result = web::NSObjectFromValueResult(nullptr);
  EXPECT_FALSE(wk_result);
}

// Tests that NSObjectFromValueResult converts Value::Type::STRING to NSString.
TEST_F(WebViewJsUtilsTest, NSObjectFromStringValueResult) {
  auto value = std::make_unique<base::Value>("test");
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSString class]]);
  EXPECT_NSEQ(@"test", wk_result);
}

// Tests that NSObjectFromValueResult converts Value::Type::INT to NSNumber.
TEST_F(WebViewJsUtilsTest, NSObjectFromIntValueResult) {
  auto value = std::make_unique<base::Value>(1);
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSNumber class]]);
  EXPECT_EQ(1, [wk_result intValue]);
}

// Tests that NSObjectFromValueResult converts Value::Type::DOUBLE to NSNumber.
TEST_F(WebViewJsUtilsTest, NSObjectFromDoubleValueResult) {
  auto value = std::make_unique<base::Value>(3.14);
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSNumber class]]);
  EXPECT_EQ(3.14, [wk_result doubleValue]);
}

// Tests that NSObjectFromValueResult converts Value::Type::BOOLEAN to NSNumber.
TEST_F(WebViewJsUtilsTest, NSObjectFromBoolValueResult) {
  auto value = std::make_unique<base::Value>(true);
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSNumber class]]);
  EXPECT_EQ(YES, [wk_result boolValue]);

  value.reset(new base::Value(false));
  wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSNumber class]]);
  EXPECT_EQ(NO, [wk_result boolValue]);
}

// Tests that NSObjectFromValueResult converts Value::Type::NONE to NSNull.
TEST_F(WebViewJsUtilsTest, NSObjectFromNoneValueResult) {
  auto value = std::make_unique<base::Value>();
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSNull class]]);
}

// Tests that NSObjectFromValueResult converts Value::Type::DICT to
// NSDictionary.
TEST_F(WebViewJsUtilsTest, NSObjectFromDictValueResult) {
  base::DictValue test_dict;
  test_dict.Set("Key1", "Value1");

  base::DictValue inner_test_dict;
  inner_test_dict.Set("Key3", 42);
  test_dict.Set("Key2", std::move(inner_test_dict));

  auto value = std::make_unique<base::Value>(std::move(test_dict));
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSDictionary class]]);

  NSDictionary* wk_result_dictionary =
      base::apple::ObjCCastStrict<NSDictionary>(wk_result);
  EXPECT_NSEQ(@"Value1", wk_result_dictionary[@"Key1"]);

  NSDictionary* inner_dictionary = wk_result_dictionary[@"Key2"];
  EXPECT_TRUE(inner_dictionary);
  EXPECT_NSEQ(@(42), inner_dictionary[@"Key3"]);
}

// Tests that NSDictionaryFromValue converts base::DictValue to NSDictionary.
TEST_F(WebViewJsUtilsTest, NSDictionaryFromValue) {
  base::DictValue test_dict;
  test_dict.Set("Key1", "Value1");

  base::DictValue inner_test_dict;
  inner_test_dict.Set("Key3", 42);
  test_dict.Set("Key2", std::move(inner_test_dict));

  id wk_result = web::NSDictionaryFromValue(test_dict);
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSDictionary class]]);

  NSDictionary* wk_result_dictionary =
      base::apple::ObjCCastStrict<NSDictionary>(wk_result);
  EXPECT_NSEQ(@"Value1", wk_result_dictionary[@"Key1"]);

  NSDictionary* inner_dictionary = wk_result_dictionary[@"Key2"];
  EXPECT_TRUE(inner_dictionary);
  EXPECT_NSEQ(@(42), inner_dictionary[@"Key3"]);
}

// Tests that NSDictionaryFromValue converts empty base::DictValue to empty
// NSDictionary.
TEST_F(WebViewJsUtilsTest, NSDictionaryFromEmptyValue) {
  base::DictValue empty_dict;
  id wk_result = web::NSDictionaryFromValue(empty_dict);
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSDictionary class]]);
  EXPECT_NSEQ(@{}, wk_result);
}

// Tests that NSObjectFromValueResult converts Value::Type::LIST to NSArray.
TEST_F(WebViewJsUtilsTest, NSObjectFromListValueResult) {
  base::ListValue test_list;
  test_list.Append("Value1");

  base::ListValue inner_test_list;
  inner_test_list.Append(true);
  test_list.Append(std::move(inner_test_list));

  test_list.Append(42);

  auto value = std::make_unique<base::Value>(std::move(test_list));
  id wk_result = web::NSObjectFromValueResult(value.get());
  EXPECT_TRUE(wk_result);
  EXPECT_TRUE([wk_result isKindOfClass:[NSArray class]]);

  NSArray* wk_result_array = base::apple::ObjCCastStrict<NSArray>(wk_result);

  EXPECT_EQ(3UL, wk_result_array.count);
  EXPECT_NSEQ(@"Value1", wk_result_array[0]);

  NSArray* inner_array = wk_result_array[1];
  EXPECT_TRUE(inner_array);
  EXPECT_TRUE([inner_array isKindOfClass:[NSArray class]]);

  EXPECT_NSEQ(@(42), wk_result_array[2]);
}

// 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 the page
// content world and web frame are both specified.
TEST_F(WebViewJsUtilsTest, ExecuteJavaScriptPageContentWorld) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);
  ASSERT_TRUE(frame_info);

  __block bool complete = false;
  __block id result = nil;
  __block NSError* error = nil;

  __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 innerResult, NSError* innerError) {
                           set_value_error = [innerError 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) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);
  ASSERT_TRUE(frame_info);

  __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]);
}

// Sets up the mock script `kMockGetExistingFramesScript` in the given web view,
// frame, and content world.
void SetupMockGetExistingFramesScript(WKWebView* web_view,
                                      WKFrameInfo* frame_info,
                                      WKContentWorld* content_world) {
  __block bool js_execution_complete = false;
  web::ExecuteJavaScript(web_view, content_world, frame_info,
                         kMockGetExistingFramesScript,
                         ^(id block_result, NSError* block_error) {
                           ASSERT_FALSE(block_error);
                           js_execution_complete = true;
                         });
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return js_execution_complete;
  }));
}

void AddUserScriptsForWorld(WKUserContentController* user_content_controller,
                            WKContentWorld* content_world) {
  WKUserScript* shared_script = [[WKUserScript alloc]
      initWithSource:web::test::GetPageScript(@"gcrweb")
      injectionTime:WKUserScriptInjectionTimeAtDocumentStart
      forMainFrameOnly:NO
      inContentWorld:content_world];
  [user_content_controller addUserScript:shared_script];
}

// Tests that `getExistingFrames` is called in the specified
// world.
TEST_F(WebViewJsUtilsTest, RegisterExistingFrames) {
  // Create a configuration and add scripts for both content worlds.
  WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
  WKUserContentController* user_content_controller =
      configuration.userContentController;

  AddUserScriptsForWorld(user_content_controller, WKContentWorld.pageWorld);
  AddUserScriptsForWorld(user_content_controller,
                         WKContentWorld.defaultClientWorld);

  // Create the WebView and load a page to trigger script injection.
  WKWebView* web_view = [[WKWebView alloc] initWithFrame:CGRectZero
                                           configuration:configuration];
  ASSERT_TRUE(
      web::test::LoadHtml(web_view, @"<html></html>",
                          [NSURL URLWithString:@"https://chromium.test/"]));
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);
  ASSERT_TRUE(frame_info);

  // Create mock __gCrWeb.getExistingFrames() in both content worlds.
  SetupMockGetExistingFramesScript(web_view, frame_info,
                                   WKContentWorld.pageWorld);
  SetupMockGetExistingFramesScript(web_view, frame_info,
                                   WKContentWorld.defaultClientWorld);

  // Verify that getExistingFrames is correctly called in the page world. Only
  // WKContentWorld.pageWorld should receive the call.
  web::RegisterExistingFrames(web_view, WKContentWorld.pageWorld);
  EXPECT_EQ(1, GetExistingFramesScriptCallCount(web_view, frame_info,
                                                WKContentWorld.pageWorld));
  EXPECT_EQ(0, GetExistingFramesScriptCallCount(
                   web_view, frame_info, WKContentWorld.defaultClientWorld));

  // Verify that getExistingFrames is correctly called in an isolated world.
  // WKContentWorld.pageWorld should not receive another call, but
  // WKContentWorld.defaultClientWorld should now receive it.
  web::RegisterExistingFrames(web_view, WKContentWorld.defaultClientWorld);
  EXPECT_EQ(1, GetExistingFramesScriptCallCount(web_view, frame_info,
                                                WKContentWorld.pageWorld));
  EXPECT_EQ(1, GetExistingFramesScriptCallCount(
                   web_view, frame_info, WKContentWorld.defaultClientWorld));
}

// Tests that ExecuteAsyncJavaScript waits for a Promise to resolve.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptWaitsForPromise) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block id block_result = nil;

  NSString* script = @"return new Promise(resolve => {"
                     @"  setTimeout(() => resolve('resolved_value'), 100);"
                     @"});";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, /*arguments=*/nil,
                              ^(id result, NSError* error) {
                                block_result = [result copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));

  EXPECT_NSEQ(@"resolved_value", block_result);
}

// Tests that ExecuteAsyncJavaScript safely handles arguments that could
// otherwise cause JS injection.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptInjectionSafety) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block id block_result = nil;

  NSString* dangerous_string = @"'; alert(1); var x = '";
  NSDictionary* args = @{@"input" : dangerous_string};

  NSString* script = @"return input;";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, args, ^(id result, NSError* error) {
                                block_result = [result copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));

  EXPECT_NSEQ(dangerous_string, block_result);
}

// Tests that ExecuteAsyncJavaScript safely handles parameter names that could
// otherwise cause JS injection.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptParameterNameInjectionSafety) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block id block_result = nil;

  NSString* dangerous_key = @"input'; alert(1); var x = '";
  NSDictionary* args = @{dangerous_key : @"value"};

  NSString* script = @"return true;";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, args, ^(id result, NSError* error) {
                                block_result = [result copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));

  EXPECT_TRUE(complete);
  EXPECT_FALSE(block_result);
}

// Tests that passing an arbitrary custom NSObject results in an error.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptCustomObjectArgument) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block NSError* block_error = nil;

  CustomObject* custom_object = [[CustomObject alloc] init];
  NSDictionary* args = @{@"input" : custom_object};
  NSString* script = @"return input;";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, args, ^(id result, NSError* error) {
                                block_error = [error copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));
  // WKJavaScriptExceptionMessage
  // Function argument values must be one of the following types, or contain
  // only the following types: NSNumber, NSNull, NSDate, NSString, NSArray, and
  // NSDictionary
  EXPECT_TRUE(block_error);
}

// Tests that 'await' can be used directly within the script body.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptWithAwait) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block id block_result = nil;

  NSString* script = @"const asyncHelper = (val) => Promise.resolve(val * 2);"
                     @"const result = await asyncHelper(21);"
                     @"return result;";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, /*arguments=*/nil,
                              ^(id result, NSError* error) {
                                block_result = [result copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));

  EXPECT_NSEQ(@42, block_result);
}

// Tests that a rejected Promise in JavaScript results in an NSError.
TEST_F(WebViewJsUtilsTest, ExecuteAsyncJavaScriptHandlesRejection) {
  WKWebView* web_view = [[WKWebView alloc] init];
  WKFrameInfo* frame_info = GetMainFrameWKFrameInfo(web_view);

  __block bool complete = false;
  __block NSError* block_error = nil;

  NSString* script = @"return Promise.reject(new Error('Async Failure'));";

  web::ExecuteAsyncJavaScript(web_view, WKContentWorld.pageWorld, frame_info,
                              script, /*arguments=*/nil,
                              ^(id result, NSError* error) {
                                block_error = [error copy];
                                complete = true;
                              });

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return complete;
  }));

  EXPECT_TRUE(block_error);
  EXPECT_NSEQ(block_error.domain, WKErrorDomain);

  EXPECT_EQ(WKErrorJavaScriptExceptionOccurred, block_error.code);

  NSString* exception_message =
      block_error.userInfo[@"WKJavaScriptExceptionMessage"];

  EXPECT_TRUE([(exception_message ?: block_error.localizedDescription)
      containsString:@"Async Failure"]);
}

}  // namespace web
