| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/test/chromedriver/chrome/web_view_impl.h" |
| |
| #include <list> |
| #include <memory> |
| #include <queue> |
| #include <string> |
| |
| #include "base/compiler_specific.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/test/chromedriver/chrome/browser_info.h" |
| #include "chrome/test/chromedriver/chrome/devtools_client_impl.h" |
| #include "chrome/test/chromedriver/chrome/frame_tracker.h" |
| #include "chrome/test/chromedriver/chrome/page_load_strategy.h" |
| #include "chrome/test/chromedriver/chrome/status.h" |
| #include "chrome/test/chromedriver/chrome/stub_devtools_client.h" |
| #include "chrome/test/chromedriver/net/sync_websocket.h" |
| #include "chrome/test/chromedriver/net/sync_websocket_factory.h" |
| #include "chrome/test/chromedriver/net/timeout.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| const char kElementKey[] = "ELEMENT"; |
| const char kElementKeyW3C[] = "element-6066-11e4-a52e-4f735466cecf"; |
| const char kShadowRootKey[] = "shadow-6066-11e4-a52e-4f735466cecf"; |
| |
| using testing::Eq; |
| using testing::Pointee; |
| |
| template <int Code> |
| testing::AssertionResult StatusCodeIs(const Status& status) { |
| if (status.code() == Code) { |
| return testing::AssertionSuccess(); |
| } else { |
| return testing::AssertionFailure() << status.message(); |
| } |
| } |
| |
| testing::AssertionResult StatusOk(const Status& status) { |
| return StatusCodeIs<kOk>(status); |
| } |
| |
| base::Value::Dict GenerateResponse(int backend_node_id) { |
| base::Value::Dict result; |
| result.SetByDottedPath(std::string("value.") + kElementKeyW3C, 0); |
| result.Set("status", 0); |
| std::string json; |
| base::JSONWriter::Write(result, &json); |
| base::Value::Dict dict; |
| dict.Set("value", std::move(json)); |
| base::Value::Dict node; |
| node.SetByDottedPath("value.backendNodeId", backend_node_id); |
| base::Value::List serialized_list; |
| serialized_list.Append(std::move(dict)); |
| serialized_list.Append(std::move(node)); |
| base::Value::Dict response; |
| response.SetByDottedPath("result.webDriverValue.value", |
| std::move(serialized_list)); |
| return response; |
| } |
| |
| base::Value::Dict GenerateResponseWithScriptArguments( |
| base::Value::List args, |
| const std::string& element_key) { |
| base::Value::List arr; |
| base::Value::List nodes; |
| for (base::Value& arg : args) { |
| if (!arg.is_dict()) { |
| arr.Append(std::move(arg)); |
| continue; |
| } |
| std::string* maybe_object_id = arg.GetDict().FindString("objectId"); |
| int object_id = 0xdeadbeef; |
| if (!maybe_object_id || !base::StringToInt(*maybe_object_id, &object_id)) { |
| arr.Append(std::move(arg)); |
| continue; |
| } |
| |
| base::Value::Dict node; |
| base::Value::Dict ref; |
| ref.Set(element_key, static_cast<int>(nodes.size())); |
| arr.Append(std::move(ref)); |
| node.SetByDottedPath("value.backendNodeId", object_id); |
| nodes.Append(std::move(node)); |
| } |
| |
| base::Value::Dict result; |
| result.Set("value", std::move(arr)); |
| result.Set("status", 0); |
| std::string json; |
| base::JSONWriter::Write(result, &json); |
| base::Value::Dict dict; |
| dict.Set("value", std::move(json)); |
| base::Value::List serialized_list; |
| serialized_list.Append(std::move(dict)); |
| |
| for (base::Value& node : nodes) { |
| serialized_list.Append(std::move(node)); |
| } |
| |
| base::Value::Dict response; |
| response.SetByDottedPath("result.webDriverValue.value", |
| std::move(serialized_list)); |
| return response; |
| } |
| |
| class FakeDevToolsClient : public StubDevToolsClient { |
| public: |
| explicit FakeDevToolsClient(std::string id) |
| : StubDevToolsClient(id), status_(kOk) {} |
| FakeDevToolsClient() : status_(kOk) {} |
| ~FakeDevToolsClient() override = default; |
| |
| void SetStatus(const Status& status) { status_ = status; } |
| void SetResult(const base::Value::Dict& result) { result_ = result.Clone(); } |
| |
| void SetElementKey(std::string element_key) { |
| element_key_ = std::move(element_key); |
| } |
| |
| // Overridden from DevToolsClient: |
| Status SendCommandAndGetResult(const std::string& method, |
| const base::Value::Dict& params, |
| base::Value::Dict* result) override { |
| if (status_.IsError()) |
| return status_; |
| |
| if (method == "Page.getFrameTree") { |
| // Unused padding frame |
| base::Value::Dict unused_child; |
| unused_child.SetByDottedPath("frame.id", "unused"); |
| unused_child.SetByDottedPath("frame.loaderId", "unused_loader"); |
| // Used by the dedicated tests |
| base::Value::Dict good_child; |
| good_child.SetByDottedPath("frame.id", "good"); |
| good_child.SetByDottedPath("frame.loaderId", "good_loader"); |
| // Default constructed WebViewImpl will point here |
| // Needed for the tests that are neutral to getFramTree |
| base::Value::Dict default_child; |
| default_child.SetByDottedPath("frame.id", GetOwner()->GetId()); |
| default_child.SetByDottedPath("frame.loaderId", "default_loader"); |
| // root |
| base::Value::List children; |
| children.Append(std::move(good_child)); |
| children.Append(std::move(default_child)); |
| for (base::Value& frame : extra_child_frames_) { |
| children.Append(frame.Clone()); |
| } |
| result->SetByDottedPath("frameTree.frame.id", "root"); |
| result->SetByDottedPath("frameTree.frame.loaderId", "root_loader"); |
| result->SetByDottedPath("frameTree.childFrames", std::move(children)); |
| } else if (method == "DOM.resolveNode") { |
| absl::optional<int> maybe_backend_node_id = |
| params.FindInt("backendNodeId"); |
| if (!maybe_backend_node_id) { |
| return Status{ |
| kUnknownError, |
| "backend node id is missing in DOM.resolveNode parameters"}; |
| } |
| result->SetByDottedPath("object.objectId", |
| base::NumberToString(*maybe_backend_node_id)); |
| } else if (method == "Runtime.callFunctionOn" && result_.empty()) { |
| const base::Value::List* args = params.FindList("arguments"); |
| if (args == nullptr) { |
| return Status{kInvalidArgument, |
| "arguments are not provided to Runtime.callFunctionOn"}; |
| } |
| *result = |
| GenerateResponseWithScriptArguments(args->Clone(), element_key_); |
| } else { |
| *result = result_.Clone(); |
| } |
| |
| return Status(kOk); |
| } |
| |
| void AddExtraChildFrame(base::Value::Dict frame) { |
| extra_child_frames_.Append(std::move(frame)); |
| } |
| |
| void ClearExtraChildFrames() { extra_child_frames_.clear(); } |
| |
| private: |
| Status status_; |
| base::Value::Dict result_; |
| base::Value::List extra_child_frames_; |
| std::string element_key_ = kElementKeyW3C; |
| }; |
| |
| void AssertEvalFails(const base::Value::Dict& command_result) { |
| base::Value::Dict result; |
| FakeDevToolsClient client; |
| client.SetResult(command_result); |
| Status status = internal::EvaluateScript( |
| &client, "context", std::string(), base::TimeDelta::Max(), false, result); |
| ASSERT_EQ(kUnknownError, status.code()); |
| ASSERT_TRUE(result.empty()); |
| } |
| |
| } // namespace |
| |
| TEST(EvaluateScript, CommandError) { |
| base::Value::Dict result; |
| FakeDevToolsClient client; |
| client.SetStatus(Status(kUnknownError)); |
| Status status = internal::EvaluateScript( |
| &client, "context", std::string(), base::TimeDelta::Max(), false, result); |
| ASSERT_EQ(kUnknownError, status.code()); |
| ASSERT_TRUE(result.empty()); |
| } |
| |
| TEST(EvaluateScript, MissingResult) { |
| base::Value::Dict dict; |
| ASSERT_NO_FATAL_FAILURE(AssertEvalFails(dict)); |
| } |
| |
| TEST(EvaluateScript, Throws) { |
| base::Value::Dict dict; |
| dict.SetByDottedPath("exceptionDetails.exception.className", "SyntaxError"); |
| dict.SetByDottedPath("result.type", "object"); |
| ASSERT_NO_FATAL_FAILURE(AssertEvalFails(dict)); |
| } |
| |
| TEST(EvaluateScript, Ok) { |
| base::Value::Dict result; |
| base::Value::Dict dict; |
| dict.SetByDottedPath("result.key", 100); |
| FakeDevToolsClient client; |
| client.SetResult(dict); |
| ASSERT_TRUE(internal::EvaluateScript(&client, "context", std::string(), |
| base::TimeDelta::Max(), false, result) |
| .IsOk()); |
| ASSERT_TRUE(result.contains("key")); |
| } |
| |
| TEST(EvaluateScriptAndGetValue, MissingType) { |
| std::unique_ptr<base::Value> result; |
| FakeDevToolsClient client; |
| base::Value::Dict dict; |
| dict.SetByDottedPath("result.value", 1); |
| client.SetResult(dict); |
| ASSERT_TRUE(internal::EvaluateScriptAndGetValue( |
| &client, "context", std::string(), base::TimeDelta::Max(), |
| false, &result) |
| .IsError()); |
| } |
| |
| TEST(EvaluateScriptAndGetValue, Undefined) { |
| std::unique_ptr<base::Value> result; |
| FakeDevToolsClient client; |
| base::Value::Dict dict; |
| dict.SetByDottedPath("result.type", "undefined"); |
| client.SetResult(dict); |
| Status status = internal::EvaluateScriptAndGetValue( |
| &client, "context", std::string(), base::TimeDelta::Max(), false, |
| &result); |
| ASSERT_EQ(kOk, status.code()); |
| ASSERT_TRUE(result && result->is_none()); |
| } |
| |
| TEST(EvaluateScriptAndGetValue, Ok) { |
| std::unique_ptr<base::Value> result; |
| FakeDevToolsClient client; |
| base::Value::Dict dict; |
| dict.SetByDottedPath("result.type", "integer"); |
| dict.SetByDottedPath("result.value", 1); |
| client.SetResult(dict); |
| Status status = internal::EvaluateScriptAndGetValue( |
| &client, "context", std::string(), base::TimeDelta::Max(), false, |
| &result); |
| ASSERT_EQ(kOk, status.code()); |
| ASSERT_TRUE(result && result->is_int()); |
| ASSERT_EQ(1, result->GetInt()); |
| } |
| |
| TEST(ParseCallFunctionResult, NotDict) { |
| std::unique_ptr<base::Value> result; |
| base::Value value(1); |
| ASSERT_NE(kOk, internal::ParseCallFunctionResult(value, &result).code()); |
| } |
| |
| TEST(ParseCallFunctionResult, Ok) { |
| std::unique_ptr<base::Value> result; |
| base::Value::Dict dict; |
| dict.Set("status", 0); |
| dict.Set("value", 1); |
| Status status = |
| internal::ParseCallFunctionResult(base::Value(std::move(dict)), &result); |
| ASSERT_EQ(kOk, status.code()); |
| ASSERT_TRUE(result && result->is_int()); |
| ASSERT_EQ(1, result->GetInt()); |
| } |
| |
| TEST(ParseCallFunctionResult, ScriptError) { |
| std::unique_ptr<base::Value> result; |
| base::Value::Dict dict; |
| dict.Set("status", 1); |
| dict.Set("value", 1); |
| Status status = |
| internal::ParseCallFunctionResult(base::Value(std::move(dict)), &result); |
| ASSERT_EQ(1, status.code()); |
| ASSERT_FALSE(result); |
| } |
| |
| namespace { |
| |
| class MockSyncWebSocket : public SyncWebSocket { |
| public: |
| explicit MockSyncWebSocket(SyncWebSocket::StatusCode next_status) |
| : connected_(false), id_(-1), next_status_(next_status) {} |
| MockSyncWebSocket() : MockSyncWebSocket(SyncWebSocket::StatusCode::kOk) {} |
| ~MockSyncWebSocket() override = default; |
| |
| void SetNexStatusCode(SyncWebSocket::StatusCode status_code) { |
| next_status_ = status_code; |
| } |
| |
| bool IsConnected() override { return connected_; } |
| |
| bool Connect(const GURL& url) override { |
| EXPECT_STREQ("http://url/", url.possibly_invalid_spec().c_str()); |
| connected_ = true; |
| return true; |
| } |
| |
| bool Send(const std::string& message) override { |
| absl::optional<base::Value> value = base::JSONReader::Read(message); |
| if (!value) { |
| return false; |
| } |
| |
| absl::optional<int> id = value->GetDict().FindInt("id"); |
| if (!id) { |
| return false; |
| } |
| |
| std::string response_str; |
| base::Value::Dict response; |
| response.Set("id", *id); |
| base::Value::Dict result; |
| result.Set("param", 1); |
| response.Set("result", std::move(result)); |
| base::JSONWriter::Write(response, &response_str); |
| messages_.push(response_str); |
| return true; |
| } |
| |
| SyncWebSocket::StatusCode ReceiveNextMessage( |
| std::string* message, |
| const Timeout& timeout) override { |
| if (next_status_ == SyncWebSocket::StatusCode::kOk && !messages_.empty()) { |
| *message = messages_.front(); |
| messages_.pop(); |
| } |
| return next_status_; |
| } |
| |
| bool HasNextMessage() override { return !messages_.empty(); } |
| |
| protected: |
| bool connected_; |
| int id_; |
| std::queue<std::string> messages_; |
| SyncWebSocket::StatusCode next_status_; |
| }; |
| |
| std::unique_ptr<SyncWebSocket> CreateMockSyncWebSocket( |
| SyncWebSocket::StatusCode next_status) { |
| return std::make_unique<MockSyncWebSocket>(next_status); |
| } |
| |
| class SyncWebSocketWrapper : public SyncWebSocket { |
| public: |
| explicit SyncWebSocketWrapper(SyncWebSocket* socket) : socket_(socket) {} |
| ~SyncWebSocketWrapper() override = default; |
| |
| bool IsConnected() override { return socket_->IsConnected(); } |
| |
| bool Connect(const GURL& url) override { return socket_->Connect(url); } |
| |
| bool Send(const std::string& message) override { |
| return socket_->Send(message); |
| } |
| |
| SyncWebSocket::StatusCode ReceiveNextMessage( |
| std::string* message, |
| const Timeout& timeout) override { |
| return socket_->ReceiveNextMessage(message, timeout); |
| } |
| |
| bool HasNextMessage() override { return socket_->HasNextMessage(); } |
| |
| private: |
| raw_ptr<SyncWebSocket> socket_; |
| }; |
| |
| } // namespace |
| |
| TEST(CreateChild, MultiLevel) { |
| SyncWebSocketFactory factory = base::BindRepeating( |
| &CreateMockSyncWebSocket, SyncWebSocket::StatusCode::kOk); |
| // CreateChild relies on client_ being a DevToolsClientImpl, so no mocking |
| std::unique_ptr<DevToolsClientImpl> client_uptr = |
| std::make_unique<DevToolsClientImpl>("id", "", "http://url", factory); |
| DevToolsClientImpl* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl level1(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kEager); |
| Status status = client_ptr->Connect(); |
| ASSERT_EQ(kOk, status.code()) << status.message(); |
| std::string sessionid = "2"; |
| std::unique_ptr<WebViewImpl> level2 = |
| std::unique_ptr<WebViewImpl>(level1.CreateChild(sessionid, "1234")); |
| level2->AttachTo(client_ptr); |
| sessionid = "3"; |
| std::unique_ptr<WebViewImpl> level3 = |
| std::unique_ptr<WebViewImpl>(level2->CreateChild(sessionid, "3456")); |
| level3->AttachTo(client_ptr); |
| sessionid = "4"; |
| std::unique_ptr<WebViewImpl> level4 = |
| std::unique_ptr<WebViewImpl>(level3->CreateChild(sessionid, "5678")); |
| level4->AttachTo(client_ptr); |
| } |
| |
| TEST(CreateChild, IsNonBlocking_NoErrors) { |
| SyncWebSocketFactory factory = base::BindRepeating( |
| &CreateMockSyncWebSocket, SyncWebSocket::StatusCode::kOk); |
| // CreateChild relies on client_ being a DevToolsClientImpl, so no mocking |
| std::unique_ptr<DevToolsClientImpl> client_uptr = |
| std::make_unique<DevToolsClientImpl>("id", "", "http://url", factory); |
| DevToolsClientImpl* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kEager); |
| Status status = client_ptr->Connect(); |
| ASSERT_EQ(kOk, status.code()) << status.message(); |
| ASSERT_FALSE(parent_view.IsNonBlocking()); |
| |
| std::string sessionid = "2"; |
| std::unique_ptr<WebViewImpl> child_view = |
| std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234")); |
| child_view->AttachTo(client_ptr); |
| ASSERT_NO_FATAL_FAILURE(child_view->IsNonBlocking()); |
| ASSERT_FALSE(child_view->IsNonBlocking()); |
| } |
| |
| TEST(CreateChild, Load_NoErrors) { |
| SyncWebSocketFactory factory = base::BindRepeating( |
| &CreateMockSyncWebSocket, SyncWebSocket::StatusCode::kOk); |
| // CreateChild relies on client_ being a DevToolsClientImpl, so no mocking |
| std::unique_ptr<DevToolsClientImpl> client_uptr = |
| std::make_unique<DevToolsClientImpl>("id", "", "http://url", factory); |
| DevToolsClientImpl* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kNone); |
| Status status = client_ptr->Connect(); |
| ASSERT_EQ(kOk, status.code()) << status.message(); |
| std::string sessionid = "2"; |
| std::unique_ptr<WebViewImpl> child_view = |
| std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234")); |
| child_view->AttachTo(client_ptr); |
| |
| ASSERT_NO_FATAL_FAILURE(child_view->Load("chrome://version", nullptr)); |
| } |
| |
| TEST(CreateChild, WaitForPendingNavigations_NoErrors) { |
| std::unique_ptr<MockSyncWebSocket> socket = |
| std::make_unique<MockSyncWebSocket>(SyncWebSocket::StatusCode::kOk); |
| SyncWebSocketFactory factory = base::BindRepeating( |
| [](SyncWebSocket* socket) { |
| return std::unique_ptr<SyncWebSocket>(new SyncWebSocketWrapper(socket)); |
| }, |
| socket.get()); |
| // CreateChild relies on client_ being a DevToolsClientImpl, so no mocking |
| std::unique_ptr<DevToolsClientImpl> client_uptr = |
| std::make_unique<DevToolsClientImpl>("id", "", "http://url", factory); |
| DevToolsClientImpl* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kNone); |
| Status status = client_ptr->Connect(); |
| ASSERT_EQ(kOk, status.code()) << status.message(); |
| std::string sessionid = "2"; |
| std::unique_ptr<WebViewImpl> child_view = |
| std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234")); |
| child_view->AttachTo(client_ptr); |
| |
| // child_view gets no socket... |
| socket->SetNexStatusCode(SyncWebSocket::StatusCode::kTimeout); |
| ASSERT_NO_FATAL_FAILURE(child_view->WaitForPendingNavigations( |
| "1234", Timeout(base::Milliseconds(10)), true)); |
| } |
| |
| TEST(CreateChild, IsPendingNavigation_NoErrors) { |
| SyncWebSocketFactory factory = base::BindRepeating( |
| &CreateMockSyncWebSocket, SyncWebSocket::StatusCode::kOk); |
| // CreateChild relies on client_ being a DevToolsClientImpl, so no mocking |
| std::unique_ptr<DevToolsClientImpl> client_uptr = |
| std::make_unique<DevToolsClientImpl>("id", "", "http://url", factory); |
| DevToolsClientImpl* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl parent_view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kNormal); |
| Status status = client_ptr->Connect(); |
| ASSERT_EQ(kOk, status.code()) << status.message(); |
| std::string sessionid = "2"; |
| std::unique_ptr<WebViewImpl> child_view = |
| std::unique_ptr<WebViewImpl>(parent_view.CreateChild(sessionid, "1234")); |
| child_view->AttachTo(client_ptr); |
| |
| Timeout timeout(base::Milliseconds(10)); |
| bool result; |
| ASSERT_NO_FATAL_FAILURE(child_view->IsPendingNavigation(&timeout, &result)); |
| } |
| |
| TEST(ManageCookies, AddCookie_SameSiteTrue) { |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>(); |
| FakeDevToolsClient* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kEager); |
| std::string samesite = "Strict"; |
| base::Value::Dict dict; |
| dict.Set("success", true); |
| client_ptr->SetResult(dict); |
| Status status = view.AddCookie("utest", "chrome://version", "value", "domain", |
| "path", samesite, true, true, 123456789); |
| ASSERT_EQ(kOk, status.code()); |
| } |
| |
| TEST(GetBackendNodeId, W3C) { |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>(); |
| FakeDevToolsClient* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl view(client_ptr->GetId(), true, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kEager); |
| { |
| base::Value::Dict node_ref; |
| node_ref.Set(kElementKey, "one_element_25"); |
| node_ref.Set(kElementKeyW3C, "one_element_13"); |
| int backend_node_id = -1; |
| EXPECT_TRUE(StatusOk(view.GetBackendNodeIdByElement( |
| "", base::Value(std::move(node_ref)), &backend_node_id))); |
| EXPECT_EQ(13, backend_node_id); |
| } |
| { |
| base::Value::Dict node_ref; |
| node_ref.Set(kShadowRootKey, "one_element_11"); |
| int backend_node_id = -1; |
| EXPECT_TRUE(StatusOk(view.GetBackendNodeIdByElement( |
| "", base::Value(std::move(node_ref)), &backend_node_id))); |
| EXPECT_EQ(11, backend_node_id); |
| } |
| } |
| |
| TEST(GetBackendNodeId, NonW3C) { |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>(); |
| FakeDevToolsClient* client_ptr = client_uptr.get(); |
| BrowserInfo browser_info; |
| WebViewImpl view(client_ptr->GetId(), false, nullptr, &browser_info, |
| std::move(client_uptr), absl::nullopt, |
| PageLoadStrategy::kEager); |
| { |
| base::Value::Dict node_ref; |
| node_ref.Set(kElementKey, "one_element_25"); |
| node_ref.Set(kElementKeyW3C, "one_element_13"); |
| int backend_node_id = -1; |
| EXPECT_TRUE(StatusOk(view.GetBackendNodeIdByElement( |
| "", base::Value(std::move(node_ref)), &backend_node_id))); |
| EXPECT_EQ(25, backend_node_id); |
| } |
| { |
| base::Value::Dict node_ref; |
| node_ref.Set(kShadowRootKey, "one_element_11"); |
| int backend_node_id = -1; |
| EXPECT_TRUE(StatusOk(view.GetBackendNodeIdByElement( |
| "", base::Value(std::move(node_ref)), &backend_node_id))); |
| EXPECT_EQ(11, backend_node_id); |
| } |
| } |
| |
| TEST(CallUserSyncScript, ElementIdAsResultRootFrame) { |
| BrowserInfo browser_info; |
| std::unique_ptr<base::Value> result; |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>("root"); |
| client_uptr->SetResult(GenerateResponse(4321)); |
| WebViewImpl view("root", true, nullptr, &browser_info, std::move(client_uptr), |
| absl::nullopt, PageLoadStrategy::kEager); |
| view.GetFrameTracker()->SetContextIdForFrame("root", "irrelevant"); |
| view.GetFrameTracker()->SetContextIdForFrame("good", "irrelevant"); |
| { |
| EXPECT_TRUE(StatusOk( |
| view.CallUserSyncScript("root", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("root_loader_element_4321"))); |
| } |
| result.reset(); |
| { |
| EXPECT_TRUE( |
| StatusOk(view.CallUserSyncScript("", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("root_loader_element_4321"))); |
| } |
| result.reset(); |
| { |
| EXPECT_TRUE(StatusOk( |
| view.CallUserSyncScript("good", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("good_loader_element_4321"))); |
| } |
| } |
| |
| TEST(CallUserSyncScript, ElementIdAsResultChildFrame) { |
| BrowserInfo browser_info; |
| std::unique_ptr<base::Value> result; |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>("good"); |
| client_uptr->SetResult(GenerateResponse(4321)); |
| WebViewImpl view("good", true, nullptr, &browser_info, std::move(client_uptr), |
| absl::nullopt, PageLoadStrategy::kEager); |
| view.GetFrameTracker()->SetContextIdForFrame("root", "irrelevant"); |
| view.GetFrameTracker()->SetContextIdForFrame("good", "irrelevant"); |
| { |
| EXPECT_TRUE(StatusOk( |
| view.CallUserSyncScript("root", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("root_loader_element_4321"))); |
| } |
| result.reset(); |
| { |
| EXPECT_TRUE( |
| StatusOk(view.CallUserSyncScript("", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("good_loader_element_4321"))); |
| } |
| result.reset(); |
| { |
| EXPECT_TRUE(StatusOk( |
| view.CallUserSyncScript("good", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| ASSERT_TRUE(result->is_dict()); |
| EXPECT_THAT(result->GetDict().FindString(kElementKeyW3C), |
| Pointee(Eq("good_loader_element_4321"))); |
| } |
| } |
| |
| TEST(CallUserSyncScript, ElementIdAsResultChildFrameErrors) { |
| BrowserInfo browser_info; |
| std::unique_ptr<base::Value> result; |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>("root"); |
| FakeDevToolsClient* client_ptr = client_uptr.get(); |
| WebViewImpl view("root", true, nullptr, &browser_info, std::move(client_uptr), |
| absl::nullopt, PageLoadStrategy::kEager); |
| view.GetFrameTracker()->SetContextIdForFrame("root", "irrelevant"); |
| view.GetFrameTracker()->SetContextIdForFrame("good", "irrelevant"); |
| view.GetFrameTracker()->SetContextIdForFrame("bad", "irrelevant"); |
| { |
| // missing frame.id case |
| base::Value::Dict bad_child; |
| bad_child.SetByDottedPath("frame.loaderId", "bad_loader"); |
| client_ptr->AddExtraChildFrame(std::move(bad_child)); |
| EXPECT_FALSE(view.CallUserSyncScript("bad", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| result.reset(); |
| { |
| // missing loaderId case |
| base::Value::Dict bad_child; |
| bad_child.SetByDottedPath("frame.id", "bad"); |
| client_ptr->AddExtraChildFrame(std::move(bad_child)); |
| EXPECT_FALSE(view.CallUserSyncScript("bad", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| result.reset(); |
| { |
| // empty frame description |
| base::Value::Dict bad_child; |
| client_ptr->AddExtraChildFrame(std::move(bad_child)); |
| EXPECT_FALSE(view.CallUserSyncScript("bad", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| result.reset(); |
| { |
| // frame is not a dictionary |
| base::Value::Dict bad_child; |
| bad_child.Set("frame", "bad"); |
| client_ptr->AddExtraChildFrame(std::move(bad_child)); |
| EXPECT_FALSE(view.CallUserSyncScript("bad", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| result.reset(); |
| { |
| // frame does not exist |
| EXPECT_FALSE(view.CallUserSyncScript("non_existent_frame", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| } |
| result.reset(); |
| { |
| // no execution context information |
| base::Value::Dict bad_child; |
| bad_child.SetByDottedPath("frame.id", "no_execution_context"); |
| bad_child.SetByDottedPath("frame.loaderId", "no_execution_context_loader"); |
| client_ptr->AddExtraChildFrame(std::move(bad_child)); |
| EXPECT_FALSE(view.CallUserSyncScript("no_execution_context", "some_code", |
| base::Value::List(), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| result.reset(); |
| { |
| // Test self-check: make sure that one shot frames work correctly |
| base::Value::Dict another_good_child; |
| another_good_child.SetByDottedPath("frame.id", "bad"); |
| another_good_child.SetByDottedPath("frame.loaderId", "bad_loader"); |
| client_ptr->AddExtraChildFrame(std::move(another_good_child)); |
| EXPECT_TRUE(StatusOk( |
| view.CallUserSyncScript("bad", "some_code", base::Value::List(), |
| base::TimeDelta::Max(), &result))); |
| client_ptr->ClearExtraChildFrames(); |
| } |
| } |
| |
| class CallUserSyncScriptArgs |
| : public testing::TestWithParam<std::pair<std::string, bool>> { |
| public: |
| void SetUp() override { |
| std::unique_ptr<FakeDevToolsClient> client_uptr = |
| std::make_unique<FakeDevToolsClient>("root"); |
| client_ptr = client_uptr.get(); |
| view = std::make_unique<WebViewImpl>( |
| "root", IsW3C(), nullptr, &browser_info, std::move(client_uptr), |
| absl::nullopt, PageLoadStrategy::kEager); |
| view->GetFrameTracker()->SetContextIdForFrame("root", "irrelevant"); |
| view->GetFrameTracker()->SetContextIdForFrame("good", "irrelevant"); |
| view->GetFrameTracker()->SetContextIdForFrame("bad", "irrelevant"); |
| } |
| |
| void TearDown() override { |
| client_ptr = nullptr; |
| view.reset(); |
| } |
| |
| bool IsW3C() { return GetParam().second; } |
| |
| std::string ElementKey() { return GetParam().first; } |
| |
| BrowserInfo browser_info; |
| std::unique_ptr<WebViewImpl> view; |
| base::raw_ptr<FakeDevToolsClient> client_ptr; |
| }; |
| |
| TEST_P(CallUserSyncScriptArgs, Root) { |
| // Expecting success as the frame and loader_id match each other. |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "root_loader_element_99"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| EXPECT_TRUE(StatusOk(view->CallUserSyncScript( |
| "root", "some_code", std::move(args), base::TimeDelta::Max(), &result))); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, GoodChild) { |
| // Expecting success as the frame and loader_id match each other. |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "good_loader_element_99"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| EXPECT_TRUE(StatusOk(view->CallUserSyncScript( |
| "good", "some_code", std::move(args), base::TimeDelta::Max(), &result))); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, DeepElement) { |
| std::string element_id = "good_loader_element_99"; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), element_id); |
| base::Value::List list; |
| list.Append(1); |
| list.Append(std::move(ref)); |
| list.Append("xyz"); |
| base::Value::Dict arg; |
| arg.SetByDottedPath("uno.a", "b"); |
| arg.SetByDottedPath("uno.dos", std::move(list)); |
| arg.SetByDottedPath("dos.b", 7.7); |
| base::Value::List nodes; |
| base::Value::List args; |
| args.Append(std::move(arg)); |
| std::unique_ptr<base::Value> result; |
| client_ptr->SetElementKey(ElementKey()); |
| EXPECT_TRUE( |
| StatusOk(view->CallUserSyncScript("good", "return nodes", std::move(args), |
| base::TimeDelta::Max(), &result))); |
| |
| ASSERT_TRUE(result->is_list()); |
| ASSERT_EQ(result->GetList().size(), size_t(1)); |
| base::Value::Dict* received_ref = result->GetList()[0].GetIfDict(); |
| ASSERT_NE(received_ref, nullptr); |
| std::string* maybe_backend_node_id = received_ref->FindString(ElementKey()); |
| EXPECT_THAT(maybe_backend_node_id, Pointee(Eq(element_id))); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, FrameAndLoaderMismatch) { |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "root_loader_element_99"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| const int expected_error = ElementKey() == kShadowRootKey |
| ? kDetachedShadowRoot |
| : kStaleElementReference; |
| EXPECT_EQ(expected_error, |
| view->CallUserSyncScript("good", "some_code", std::move(args), |
| base::TimeDelta::Max(), &result) |
| .code()); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, NoSuchLoader) { |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "bad_loader_element_99"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| const int expected_error = ElementKey() == kShadowRootKey |
| ? kDetachedShadowRoot |
| : kStaleElementReference; |
| EXPECT_EQ(expected_error, |
| view->CallUserSyncScript("root", "some_code", std::move(args), |
| base::TimeDelta::Max(), &result) |
| .code()); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, IncorrectSeparator) { |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "good_loader_eeeeeee_99"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| EXPECT_FALSE(view->CallUserSyncScript("good", "some_code", std::move(args), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, NoBackendNodeId) { |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "good_loader_element_xx"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| EXPECT_FALSE(view->CallUserSyncScript("good", "some_code", std::move(args), |
| base::TimeDelta::Max(), &result) |
| .IsOk()); |
| } |
| |
| TEST_P(CallUserSyncScriptArgs, RepeatedSeparator) { |
| // We have a test that checks if the second component (BackendNodeId) is |
| // integer. |
| // Here it is sufficient to check that the call does not fail. |
| // This would mean that only the last _element_ id was used as a separation |
| // point. |
| base::Value::Dict extra_child; |
| extra_child.SetByDottedPath("frame.id", "extra_child_element_some_text"); |
| extra_child.SetByDottedPath("frame.loaderId", |
| "extra_child_element_some_text_loader"); |
| client_ptr->AddExtraChildFrame(std::move(extra_child)); |
| view->GetFrameTracker()->SetContextIdForFrame("extra_child_element_some_text", |
| "irrelevant"); |
| base::Value::List args; |
| base::Value::Dict ref; |
| ref.Set(ElementKey(), "extra_child_element_some_text_loader_element_71"); |
| args.Append(std::move(ref)); |
| std::unique_ptr<base::Value> result; |
| EXPECT_TRUE(StatusOk(view->CallUserSyncScript( |
| "extra_child_element_some_text", "some_code", std::move(args), |
| base::TimeDelta::Max(), &result))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(References, |
| CallUserSyncScriptArgs, |
| ::testing::Values(std::make_pair(kElementKeyW3C, true), |
| std::make_pair(kShadowRootKey, true), |
| std::make_pair(kElementKey, false), |
| std::make_pair(kShadowRootKey, |
| false))); |