#include "extensions/renderer/bindings/api_last_error.h"
#include <optional>
#include <string_view>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "extensions/renderer/bindings/api_binding_test.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "gin/converter.h"
#include "gin/public/context_holder.h"
namespace extensions {
namespace {
v8::Local<v8::Value> GetLastError(v8::Local<v8::Object> parent,
v8::Local<v8::Context> context) {
return GetPropertyFromObject(parent, context, "lastError");
}
std::string GetLastErrorMessage(v8::Local<v8::Object> parent,
v8::Local<v8::Context> context) {
v8::Local<v8::Value> last_error = GetLastError(parent, context);
if (last_error.IsEmpty() || !last_error->IsObject())
return V8ToString(last_error, context);
v8::Local<v8::Value> message =
GetPropertyFromObject(last_error.As<v8::Object>(), context, "message");
return V8ToString(message, context);
}
using ContextParentPair =
std::pair<v8::Local<v8::Context>, v8::Local<v8::Object>>;
using ParentList = v8::MemorySpan<ContextParentPair>;
v8::Local<v8::Object> GetParent(const ParentList& parents,
v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent) {
for (const auto& parent : parents) {
if (parent.first == context)
return parent.second;
}
return v8::Local<v8::Object>();
}
}
using APILastErrorTest = APIBindingTest;
TEST_F(APILastErrorTest, TestLastError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> parent_object = v8::Object::New(isolate());
auto parents = v8::to_array<ContextParentPair>({{context, parent_object}});
APILastError last_error(base::BindRepeating(&GetParent, ParentList(parents)),
base::DoNothing());
EXPECT_FALSE(last_error.HasError(context));
EXPECT_FALSE(last_error.GetErrorMessage(context));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_object, context));
EXPECT_FALSE(
parent_object->Has(context, gin::StringToV8(isolate(), "lastError"))
.ToChecked());
last_error.SetError(context, "Some last error");
EXPECT_TRUE(last_error.HasError(context));
EXPECT_EQ(R"("Some last error")",
GetLastErrorMessage(parent_object, context));
std::optional<std::string> error_message =
last_error.GetErrorMessage(context);
EXPECT_TRUE(error_message);
EXPECT_EQ("Some last error", error_message);
last_error.ClearError(context, false);
EXPECT_FALSE(last_error.HasError(context));
EXPECT_FALSE(last_error.GetErrorMessage(context));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_object, context));
EXPECT_FALSE(
parent_object->Has(context, gin::StringToV8(isolate(), "lastError"))
.ToChecked());
}
TEST_F(APILastErrorTest, ReportIfUnchecked) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> parent_object = v8::Object::New(isolate());
std::optional<std::string> console_error;
auto log_error = [](std::optional<std::string>* console_error,
v8::Local<v8::Context> context,
const std::string& error) { *console_error = error; };
auto parents = v8::to_array<ContextParentPair>({{context, parent_object}});
APILastError last_error(base::BindRepeating(&GetParent, ParentList(parents)),
base::BindRepeating(log_error, &console_error));
{
v8::TryCatch try_catch(isolate());
last_error.SetError(context, "foo");
EXPECT_EQ("\"foo\"", GetLastErrorMessage(parent_object, context));
last_error.ClearError(context, true);
EXPECT_FALSE(console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate());
last_error.SetError(context, "foo");
v8::Local<v8::Value> v8_error = GetLastError(parent_object, context);
ASSERT_FALSE(v8_error.IsEmpty());
EXPECT_TRUE(v8_error->IsObject());
last_error.ClearError(context, true);
EXPECT_FALSE(console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate());
last_error.SetError(context, "A last error");
last_error.ClearError(context, true);
ASSERT_TRUE(console_error);
EXPECT_EQ("Unchecked runtime.lastError: A last error", *console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate());
last_error.SetError(context, "A last error");
EXPECT_TRUE(last_error.HasError(context));
last_error.ClearError(context, true);
ASSERT_TRUE(console_error);
EXPECT_EQ("Unchecked runtime.lastError: A last error", *console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
{
v8::TryCatch try_catch(isolate());
last_error.SetError(context, "A last error");
std::optional<std::string> error_message =
last_error.GetErrorMessage(context);
EXPECT_TRUE(error_message);
EXPECT_EQ("A last error", error_message);
last_error.ClearError(context, true);
ASSERT_TRUE(console_error);
EXPECT_EQ("Unchecked runtime.lastError: A last error", *console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
}
TEST_F(APILastErrorTest, ReportUncheckedError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> parent_object = v8::Object::New(isolate());
std::optional<std::string> console_error;
auto log_error = [](std::optional<std::string>* console_error,
v8::Local<v8::Context> context,
const std::string& error) { *console_error = error; };
auto parents = v8::to_array<ContextParentPair>({{context, parent_object}});
APILastError last_error(base::BindRepeating(&GetParent, ParentList(parents)),
base::BindRepeating(log_error, &console_error));
EXPECT_FALSE(last_error.HasError(context));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_object, context));
EXPECT_FALSE(
parent_object->Has(context, gin::StringToV8(isolate(), "lastError"))
.ToChecked());
{
v8::TryCatch try_catch(isolate());
last_error.ReportUncheckedError(context, "A last error");
ASSERT_TRUE(console_error);
EXPECT_EQ("Unchecked runtime.lastError: A last error", *console_error);
EXPECT_FALSE(try_catch.HasCaught());
}
EXPECT_FALSE(last_error.HasError(context));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_object, context));
EXPECT_FALSE(
parent_object->Has(context, gin::StringToV8(isolate(), "lastError"))
.ToChecked());
}
TEST_F(APILastErrorTest, NonLastErrorObject) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> parent_object = v8::Object::New(isolate());
auto parents = v8::to_array<ContextParentPair>({{context, parent_object}});
APILastError last_error(base::BindRepeating(&GetParent, ParentList(parents)),
base::DoNothing());
auto checked_set = [context](v8::Local<v8::Object> object,
std::string_view key,
v8::Local<v8::Value> value) {
v8::Maybe<bool> success = object->Set(
context, gin::StringToSymbol(v8::Isolate::GetCurrent(), key), value);
ASSERT_TRUE(success.IsJust());
ASSERT_TRUE(success.FromJust());
};
v8::Local<v8::Object> fake_last_error = v8::Object::New(isolate());
checked_set(fake_last_error, "message",
gin::StringToV8(isolate(), "fake error"));
checked_set(parent_object, "lastError", fake_last_error);
EXPECT_EQ("\"fake error\"", GetLastErrorMessage(parent_object, context));
last_error.SetError(context, "Real last error");
EXPECT_EQ("\"fake error\"", GetLastErrorMessage(parent_object, context));
last_error.ClearError(context, false);
EXPECT_EQ("\"fake error\"", GetLastErrorMessage(parent_object, context));
checked_set(parent_object, "lastError", v8::Undefined(isolate()));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_object, context));
last_error.SetError(context, "a last error");
EXPECT_EQ("\"a last error\"", GetLastErrorMessage(parent_object, context));
checked_set(parent_object, "lastError", fake_last_error);
EXPECT_EQ("\"fake error\"", GetLastErrorMessage(parent_object, context));
}
TEST_F(APILastErrorTest, MultipleContexts) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context_a = MainContext();
v8::Local<v8::Context> context_b = AddContext();
v8::Local<v8::Object> parent_a = v8::Object::New(isolate());
v8::Local<v8::Object> parent_b = v8::Object::New(isolate());
auto parents = v8::to_array<ContextParentPair>(
{{context_a, parent_a}, {context_b, parent_b}});
APILastError last_error(base::BindRepeating(&GetParent, ParentList(parents)),
base::DoNothing());
last_error.SetError(context_a, "Last error a");
EXPECT_EQ("\"Last error a\"", GetLastErrorMessage(parent_a, context_a));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_b, context_b));
last_error.SetError(context_b, "Last error b");
EXPECT_EQ("\"Last error a\"", GetLastErrorMessage(parent_a, context_a));
EXPECT_EQ("\"Last error b\"", GetLastErrorMessage(parent_b, context_b));
last_error.ClearError(context_b, false);
EXPECT_EQ("\"Last error a\"", GetLastErrorMessage(parent_a, context_a));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_b, context_b));
last_error.ClearError(context_a, false);
EXPECT_EQ("undefined", GetLastErrorMessage(parent_a, context_a));
EXPECT_EQ("undefined", GetLastErrorMessage(parent_b, context_b));
}
TEST_F(APILastErrorTest, SecondaryParent) {
auto get_parents = [](v8::Local<v8::Object> primary_parent,
v8::Local<v8::Object> secondary_parent,
v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent_out) {
if (secondary_parent_out)
*secondary_parent_out = secondary_parent;
return primary_parent;
};
std::optional<std::string> console_error;
auto log_error = [](std::optional<std::string>* console_error,
v8::Local<v8::Context> context,
const std::string& error) { *console_error = error; };
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> primary_parent = v8::Object::New(isolate());
v8::Local<v8::Object> secondary_parent = v8::Object::New(isolate());
APILastError last_error(
base::BindRepeating(get_parents, primary_parent, secondary_parent),
base::BindRepeating(log_error, &console_error));
last_error.SetError(context, "error");
EXPECT_TRUE(last_error.HasError(context));
EXPECT_EQ("\"error\"", GetLastErrorMessage(primary_parent, context));
EXPECT_EQ("\"error\"", GetLastErrorMessage(secondary_parent, context));
EXPECT_FALSE(console_error);
last_error.ClearError(context, true);
EXPECT_FALSE(console_error);
EXPECT_EQ("undefined", GetLastErrorMessage(primary_parent, context));
EXPECT_EQ("undefined", GetLastErrorMessage(secondary_parent, context));
last_error.SetError(context, "error");
EXPECT_EQ("\"error\"", GetLastErrorMessage(primary_parent, context));
last_error.ClearError(context, true);
EXPECT_FALSE(console_error);
last_error.SetError(context, "error");
EXPECT_EQ("\"error\"", GetLastErrorMessage(secondary_parent, context));
last_error.ClearError(context, true);
ASSERT_TRUE(console_error);
EXPECT_EQ("Unchecked runtime.lastError: error", *console_error);
}
}