| // 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/window_commands.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <list> |
| #include <map> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/adapters.h" |
| #include "base/containers/flat_set.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversion_utils.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/test/chromedriver/basic_types.h" |
| #include "chrome/test/chromedriver/chrome/chrome.h" |
| #include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h" |
| #include "chrome/test/chromedriver/chrome/devtools_client.h" |
| #include "chrome/test/chromedriver/chrome/geoposition.h" |
| #include "chrome/test/chromedriver/chrome/mobile_emulation_override_manager.h" |
| #include "chrome/test/chromedriver/chrome/network_conditions.h" |
| #include "chrome/test/chromedriver/chrome/status.h" |
| #include "chrome/test/chromedriver/chrome/ui_events.h" |
| #include "chrome/test/chromedriver/chrome/web_view.h" |
| #include "chrome/test/chromedriver/element_commands.h" |
| #include "chrome/test/chromedriver/element_util.h" |
| #include "chrome/test/chromedriver/key_converter.h" |
| #include "chrome/test/chromedriver/net/command_id.h" |
| #include "chrome/test/chromedriver/net/timeout.h" |
| #include "chrome/test/chromedriver/session.h" |
| #include "chrome/test/chromedriver/util.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "url/url_util.h" |
| |
| namespace { |
| |
| // The error page URL was renamed in |
| // https://chromium-review.googlesource.com/c/580169, but because ChromeDriver |
| // needs to be backward-compatible with older versions of Chrome, it is |
| // necessary to compare against both the old and new error URL. |
| static const char kUnreachableWebDataURL[] = "chrome-error://chromewebdata/"; |
| const char kDeprecatedUnreachableWebDataURL[] = "data:text/html,chromewebdata"; |
| |
| // Match to content/browser/devtools/devTools_session const of same name |
| const char kTargetClosedMessage[] = "Inspected target navigated or closed"; |
| |
| // TODO(crbug.com/chromedriver/2596): Remove when we stop supporting legacy |
| // protocol. |
| // Defaults to 20 years into the future when adding a cookie. |
| const double kDefaultCookieExpiryTime = 20*365*24*60*60; |
| |
| // for pointer actions |
| enum class PointerActionType { NOT_INITIALIZED, PRESS, MOVE, RELEASE, IDLE }; |
| |
| const base::flat_set<StatusCode> kNavigationHints = { |
| kNoSuchExecutionContext, |
| kAbortedByNavigation, |
| }; |
| |
| Status GetMouseButton(const base::Value::Dict& params, MouseButton* button) { |
| // Default to left mouse button. |
| int button_num = params.FindInt("button").value_or(0); |
| if (button_num < 0 || button_num > 2) { |
| return Status(kInvalidArgument, |
| base::StringPrintf("invalid button: %d", button_num)); |
| } |
| *button = static_cast<MouseButton>(button_num); |
| return Status(kOk); |
| } |
| |
| Status IntToStringButton(int button, std::string& out) { |
| if (button == 0) { |
| out = "left"; |
| } else if (button == 1) { |
| out = "middle"; |
| } else if (button == 2) { |
| out = "right"; |
| } else if (button == 3) { |
| out = "back"; |
| } else if (button == 4) { |
| out = "forward"; |
| } else { |
| return Status(kInvalidArgument, |
| "'button' must be an integer between 0 and 4 inclusive"); |
| } |
| return Status(kOk); |
| } |
| |
| Status GetUrl(WebView* web_view, const std::string& frame, std::string* url) { |
| std::unique_ptr<base::Value> value; |
| base::Value::List args; |
| Status status = web_view->CallFunction( |
| frame, "function() { return document.URL; }", args, &value); |
| if (status.IsError()) |
| return status; |
| if (!value->is_string()) |
| return Status(kUnknownError, "javascript failed to return the url"); |
| *url = value->GetString(); |
| return Status(kOk); |
| } |
| |
| MouseEventType StringToMouseEventType(std::string action_type) { |
| if (action_type == "pointerDown") |
| return kPressedMouseEventType; |
| if (action_type == "pointerUp") |
| return kReleasedMouseEventType; |
| if (action_type == "pointerMove") |
| return kMovedMouseEventType; |
| if (action_type == "scroll") |
| return kWheelMouseEventType; |
| if (action_type == "pause") |
| return kPauseMouseEventType; |
| return kPressedMouseEventType; |
| } |
| |
| MouseButton StringToMouseButton(std::string button_type) { |
| if (button_type == "left") |
| return kLeftMouseButton; |
| if (button_type == "middle") |
| return kMiddleMouseButton; |
| if (button_type == "right") |
| return kRightMouseButton; |
| if (button_type == "back") |
| return kBackMouseButton; |
| if (button_type == "forward") |
| return kForwardMouseButton; |
| return kNoneMouseButton; |
| } |
| |
| TouchEventType StringToTouchEventType(std::string action_type) { |
| if (action_type == "pointerDown") |
| return kTouchStart; |
| if (action_type == "pointerUp") |
| return kTouchEnd; |
| if (action_type == "pointerMove") |
| return kTouchMove; |
| if (action_type == "pointerCancel") |
| return kTouchCancel; |
| if (action_type == "pause") |
| return kPause; |
| return kTouchStart; |
| } |
| |
| int StringToModifierMouseButton(std::string button_type) { |
| if (button_type == "left") |
| return 1; |
| if (button_type == "right") |
| return 2; |
| if (button_type == "middle") |
| return 4; |
| if (button_type == "back") |
| return 8; |
| if (button_type == "forward") |
| return 16; |
| return 0; |
| } |
| |
| int MouseButtonToButtons(MouseButton button) { |
| switch (button) { |
| case kLeftMouseButton: |
| return 1; |
| case kRightMouseButton: |
| return 2; |
| case kMiddleMouseButton: |
| return 4; |
| case kBackMouseButton: |
| return 8; |
| case kForwardMouseButton: |
| return 16; |
| default: |
| return 0; |
| } |
| } |
| |
| int KeyToKeyModifiers(std::string key) { |
| if (key == "Shift") |
| return kShiftKeyModifierMask; |
| if (key == "Control") |
| return kControlKeyModifierMask; |
| if (key == "Alt") |
| return kAltKeyModifierMask; |
| if (key == "Meta") |
| return kMetaKeyModifierMask; |
| return 0; |
| } |
| |
| PointerType StringToPointerType(std::string pointer_type) { |
| CHECK(pointer_type == "pen" || pointer_type == "mouse"); |
| if (pointer_type == "pen") |
| return kPen; |
| return kMouse; |
| } |
| |
| struct Cookie { |
| Cookie(const std::string& name, |
| const std::string& value, |
| const std::string& domain, |
| const std::string& path, |
| const std::string& samesite, |
| int64_t expiry, |
| bool http_only, |
| bool secure, |
| bool session) |
| : name(name), |
| value(value), |
| domain(domain), |
| path(path), |
| samesite(samesite), |
| expiry(expiry), |
| http_only(http_only), |
| secure(secure), |
| session(session) {} |
| |
| std::string name; |
| std::string value; |
| std::string domain; |
| std::string path; |
| std::string samesite; |
| int64_t expiry; |
| bool http_only; |
| bool secure; |
| bool session; |
| }; |
| |
| base::Value::Dict CreateDictionaryFrom(const Cookie& cookie) { |
| base::Value::Dict dict; |
| dict.Set("name", cookie.name); |
| dict.Set("value", cookie.value); |
| if (!cookie.domain.empty()) |
| dict.Set("domain", cookie.domain); |
| if (!cookie.path.empty()) |
| dict.Set("path", cookie.path); |
| if (!cookie.session) |
| SetSafeInt(dict, "expiry", cookie.expiry); |
| dict.Set("httpOnly", cookie.http_only); |
| dict.Set("secure", cookie.secure); |
| if (!cookie.samesite.empty()) { |
| dict.Set("sameSite", cookie.samesite); |
| } else { |
| // The default in the standard is Lax: |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite |
| // Chrome (mostly) treats default cookies as Lax so this seems correct: |
| // https://chromestatus.com/feature/5088147346030592 |
| dict.Set("sameSite", "Lax"); |
| } |
| return dict; |
| } |
| |
| Status GetVisibleCookies(Session* for_session, |
| WebView* web_view, |
| std::list<Cookie>* cookies) { |
| std::string current_page_url; |
| Status status = |
| GetUrl(web_view, for_session->GetCurrentFrameId(), ¤t_page_url); |
| if (status.IsError()) |
| return status; |
| base::Value internal_cookies; |
| status = web_view->GetCookies(&internal_cookies, current_page_url); |
| if (status.IsError()) |
| return status; |
| std::list<Cookie> cookies_tmp; |
| for (const base::Value& cookie_value : internal_cookies.GetList()) { |
| if (!cookie_value.is_dict()) |
| return Status(kUnknownError, "DevTools returns a non-dictionary cookie"); |
| |
| const base::Value::Dict& cookie_dict = cookie_value.GetDict(); |
| |
| const std::string* name = cookie_dict.FindString("name"); |
| const std::string* value = cookie_dict.FindString("value"); |
| const std::string* domain = cookie_dict.FindString("domain"); |
| const std::string* path = cookie_dict.FindString("path"); |
| std::string samesite; |
| GetOptionalString(cookie_dict, "sameSite", &samesite); |
| int64_t expiry = |
| static_cast<int64_t>(cookie_dict.FindDouble("expires").value_or(0)); |
| // Truncate & convert the value to an integer as required by W3C spec. |
| if (expiry >= (1ll << 53) || expiry <= -(1ll << 53)) |
| expiry = 0; |
| bool http_only = cookie_dict.FindBool("httpOnly").value_or(false); |
| bool session = cookie_dict.FindBool("session").value_or(false); |
| bool secure = cookie_dict.FindBool("secure").value_or(false); |
| |
| cookies_tmp.push_back(Cookie(*name, *value, *domain, *path, samesite, |
| expiry, http_only, secure, session)); |
| } |
| cookies->swap(cookies_tmp); |
| return Status(kOk); |
| } |
| |
| Status ScrollCoordinateInToView( |
| Session* session, WebView* web_view, int x, int y, int* offset_x, |
| int* offset_y) { |
| std::unique_ptr<base::Value> value; |
| base::Value::List args; |
| args.Append(x); |
| args.Append(y); |
| Status status = web_view->CallFunction( |
| std::string(), |
| "function(x, y) {" |
| " if (x < window.pageXOffset ||" |
| " x >= window.pageXOffset + window.innerWidth ||" |
| " y < window.pageYOffset ||" |
| " y >= window.pageYOffset + window.innerHeight) {" |
| " window.scrollTo(x - window.innerWidth/2, y - window.innerHeight/2);" |
| " }" |
| " return {" |
| " view_x: Math.floor(window.pageXOffset)," |
| " view_y: Math.floor(window.pageYOffset)," |
| " view_width: Math.floor(window.innerWidth)," |
| " view_height: Math.floor(window.innerHeight)};" |
| "}", |
| args, |
| &value); |
| if (!status.IsOk()) |
| return status; |
| const base::Value::Dict& view_attrib = value->GetDict(); |
| int view_x = view_attrib.FindInt("view_x").value_or(0); |
| int view_y = view_attrib.FindInt("view_y").value_or(0); |
| int view_width = view_attrib.FindInt("view_width").value_or(0); |
| int view_height = view_attrib.FindInt("view_height").value_or(0); |
| *offset_x = x - view_x; |
| *offset_y = y - view_y; |
| if (*offset_x < 0 || *offset_x >= view_width || *offset_y < 0 || |
| *offset_y >= view_height) { |
| return Status(kUnknownError, "Failed to scroll coordinate into view"); |
| } |
| return Status(kOk); |
| } |
| |
| Status ExecuteTouchEvent(Session* session, |
| WebView* web_view, |
| TouchEventType type, |
| const base::Value::Dict& params) { |
| std::optional<int> x = params.FindInt("x"); |
| std::optional<int> y = params.FindInt("y"); |
| if (!x) |
| return Status(kInvalidArgument, "'x' must be an integer"); |
| if (!y) |
| return Status(kInvalidArgument, "'y' must be an integer"); |
| int relative_x = *x; |
| int relative_y = *y; |
| Status status = ScrollCoordinateInToView(session, web_view, *x, *y, |
| &relative_x, &relative_y); |
| if (!status.IsOk()) |
| return status; |
| std::vector<TouchEvent> events; |
| events.emplace_back(type, relative_x, relative_y); |
| return web_view->DispatchTouchEvents(events, false); |
| } |
| |
| Status WindowViewportSize(Session* session, |
| WebView* web_view, |
| int* inner_width, |
| int* inner_height) { |
| DCHECK(inner_width); |
| DCHECK(inner_height); |
| std::unique_ptr<base::Value> value; |
| base::Value::List args; |
| Status status = |
| web_view->CallFunction(std::string(), |
| "function() {" |
| " return {" |
| " view_width: Math.floor(window.innerWidth)," |
| " view_height: Math.floor(window.innerHeight)};" |
| "}", |
| args, &value); |
| if (!status.IsOk()) |
| return status; |
| const base::Value::Dict& view_attrib = value->GetDict(); |
| std::optional<int> maybe_inner_width = view_attrib.FindInt("view_width"); |
| if (maybe_inner_width) |
| *inner_width = *maybe_inner_width; |
| |
| std::optional<int> maybe_inner_height = view_attrib.FindInt("view_height"); |
| if (maybe_inner_height) |
| *inner_height = *maybe_inner_height; |
| return Status(kOk); |
| } |
| |
| Status ProcessPauseAction(const base::Value::Dict& action_item, |
| base::Value::Dict* action) { |
| int duration = 0; |
| bool has_value = false; |
| if (!GetOptionalInt(action_item, "duration", &duration, &has_value) || |
| duration < 0) |
| return Status(kInvalidArgument, "'duration' must be a non-negative int"); |
| if (has_value) |
| action->Set("duration", duration); |
| return Status(kOk); |
| } |
| |
| Status ElementInViewCenter(Session* session, |
| WebView* web_view, |
| std::string element_id, |
| int* center_x, |
| int* center_y) { |
| WebPoint center_location; |
| Status status = GetElementLocationInViewCenter(session, web_view, element_id, |
| ¢er_location); |
| if (status.IsError()) |
| return status; |
| |
| *center_x = center_location.x; |
| *center_y = center_location.y; |
| return Status(kOk); |
| } |
| |
| int GetMouseClickCount(int last_click_count, |
| float x, |
| float y, |
| float last_x, |
| float last_y, |
| int button_id, |
| int last_button_id, |
| const base::TimeTicks& timestamp, |
| const base::TimeTicks& last_mouse_click_time) { |
| const int kDoubleClickTimeMS = 500; |
| const int kDoubleClickRange = 4; |
| if (last_click_count == 0) |
| return 1; |
| |
| base::TimeDelta time_difference = timestamp - last_mouse_click_time; |
| if (time_difference.InMilliseconds() > kDoubleClickTimeMS) |
| return 1; |
| |
| if (std::abs(x - last_x) > kDoubleClickRange / 2) |
| return 1; |
| |
| if (std::abs(y - last_y) > kDoubleClickRange / 2) |
| return 1; |
| |
| if (last_button_id != button_id) |
| return 1; |
| |
| #if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_WIN) |
| // On Mac and Windows, we keep increasing the click count, but on the other |
| // platforms, we reset the count to 1 when it is greater than 3. |
| if (last_click_count >= 3) |
| return 1; |
| #endif |
| return last_click_count + 1; |
| } |
| |
| const char kLandscape[] = "landscape"; |
| const char kPortrait[] = "portrait"; |
| |
| Status ParseOrientation(const base::Value::Dict& params, |
| std::string* orientation) { |
| bool has_value; |
| if (!GetOptionalString(params, "orientation", orientation, &has_value)) { |
| return Status(kInvalidArgument, "'orientation' must be a string"); |
| } |
| |
| if (!has_value) { |
| *orientation = kPortrait; |
| } else if (*orientation != kPortrait && *orientation != kLandscape) { |
| return Status(kInvalidArgument, "'orientation' must be '" + |
| std::string(kPortrait) + "' or '" + |
| std::string(kLandscape) + "'"); |
| } |
| return Status(kOk); |
| } |
| |
| Status ParseScale(const base::Value::Dict& params, double* scale) { |
| bool has_value; |
| if (!GetOptionalDouble(params, "scale", scale, &has_value)) { |
| return Status(kInvalidArgument, "'scale' must be a double"); |
| } |
| |
| if (!has_value) { |
| *scale = 1; |
| } else if (*scale < 0.1 || *scale > 2) { |
| return Status(kInvalidArgument, "'scale' must not be < 0.1 or > 2"); |
| } |
| return Status(kOk); |
| } |
| |
| Status ParseBoolean(const base::Value::Dict& params, |
| const std::string& name, |
| bool default_value, |
| bool* b) { |
| *b = default_value; |
| if (!GetOptionalBool(params, name, b)) { |
| return Status(kInvalidArgument, "'" + name + "' must be a boolean"); |
| } |
| return Status(kOk); |
| } |
| |
| Status GetNonNegativeDouble(const base::Value::Dict& dict, |
| const std::string& parent, |
| const std::string& child, |
| double* attribute) { |
| bool has_value; |
| std::string attribute_str = "'" + parent + "." + child + "'"; |
| if (!GetOptionalDouble(dict, child, attribute, &has_value)) { |
| return Status(kInvalidArgument, attribute_str + " must be a double"); |
| } |
| |
| if (has_value) { |
| *attribute = ConvertCentimeterToInch(*attribute); |
| if (*attribute < 0) { |
| return Status(kInvalidArgument, |
| attribute_str + " must not be less than 0"); |
| } |
| } |
| return Status(kOk); |
| } |
| |
| struct Page { |
| double width; |
| double height; |
| }; |
| |
| Status ParsePage(const base::Value::Dict& params, Page* page) { |
| bool has_value; |
| const base::Value::Dict* page_dict = nullptr; |
| if (!GetOptionalDictionary(params, "page", &page_dict, &has_value)) { |
| return Status(kInvalidArgument, "'page' must be an object"); |
| } |
| page->width = ConvertCentimeterToInch(21.59); |
| page->height = ConvertCentimeterToInch(27.94); |
| if (!has_value) |
| return Status(kOk); |
| |
| Status status = |
| GetNonNegativeDouble(*page_dict, "page", "width", &page->width); |
| if (status.IsError()) |
| return status; |
| |
| status = GetNonNegativeDouble(*page_dict, "page", "height", &page->height); |
| if (status.IsError()) |
| return status; |
| |
| return Status(kOk); |
| } |
| |
| struct Margin { |
| double top; |
| double bottom; |
| double left; |
| double right; |
| }; |
| |
| Status ParseMargin(const base::Value::Dict& params, Margin* margin) { |
| bool has_value; |
| const base::Value::Dict* margin_dict = nullptr; |
| if (!GetOptionalDictionary(params, "margin", &margin_dict, &has_value)) { |
| return Status(kInvalidArgument, "'margin' must be an object"); |
| } |
| |
| margin->top = ConvertCentimeterToInch(1.0); |
| margin->bottom = ConvertCentimeterToInch(1.0); |
| margin->left = ConvertCentimeterToInch(1.0); |
| margin->right = ConvertCentimeterToInch(1.0); |
| |
| if (!has_value) |
| return Status(kOk); |
| |
| Status status = |
| GetNonNegativeDouble(*margin_dict, "margin", "top", &margin->top); |
| if (status.IsError()) |
| return status; |
| |
| status = |
| GetNonNegativeDouble(*margin_dict, "margin", "bottom", &margin->bottom); |
| if (status.IsError()) |
| return status; |
| |
| status = GetNonNegativeDouble(*margin_dict, "margin", "left", &margin->left); |
| if (status.IsError()) |
| return status; |
| |
| status = |
| GetNonNegativeDouble(*margin_dict, "margin", "right", &margin->right); |
| if (status.IsError()) |
| return status; |
| |
| return Status(kOk); |
| } |
| |
| Status ParsePageRanges(const base::Value::Dict& params, |
| std::string* page_ranges) { |
| bool has_value; |
| const base::Value::List* page_range_list = nullptr; |
| if (!GetOptionalList(params, "pageRanges", &page_range_list, &has_value)) { |
| return Status(kInvalidArgument, "'pageRanges' must be an array"); |
| } |
| |
| if (!has_value) { |
| return Status(kOk); |
| } |
| |
| std::vector<std::string> ranges; |
| for (const base::Value& page_range : *page_range_list) { |
| if (page_range.is_int()) { |
| if (page_range.GetInt() < 0) { |
| return Status(kInvalidArgument, |
| "a Number entry in 'pageRanges' must not be less than 0"); |
| } |
| ranges.push_back(base::NumberToString(page_range.GetInt())); |
| } else if (page_range.is_string()) { |
| ranges.push_back(page_range.GetString()); |
| } else { |
| return Status(kInvalidArgument, |
| "an entry in 'pageRanges' must be a Number or String"); |
| } |
| } |
| |
| *page_ranges = base::JoinString(ranges, ","); |
| return Status(kOk); |
| } |
| |
| // Returns: |
| // 1. Optional with the default value, if there is no such a key in the |
| // dictionary. |
| // 2. Empty optional, if the key is in the dictionary, but value has |
| // unexpected type. |
| // 3. Optional with value from dictionary. |
| template <typename T> |
| std::optional<T> ParseIfInDictionary( |
| const base::Value::Dict& dict, |
| std::string_view key, |
| T default_value, |
| std::optional<T> (base::Value::*getterIfType)() const) { |
| const auto* val = dict.Find(key); |
| if (!val) |
| return std::make_optional(default_value); |
| return (val->*getterIfType)(); |
| } |
| |
| std::optional<double> ParseDoubleIfInDictionary(const base::Value::Dict& dict, |
| std::string_view key, |
| double default_value) { |
| return ParseIfInDictionary(dict, key, default_value, |
| &base::Value::GetIfDouble); |
| } |
| |
| std::optional<int> ParseIntIfInDictionary(const base::Value::Dict& dict, |
| std::string_view key, |
| int default_value) { |
| return ParseIfInDictionary(dict, key, default_value, &base::Value::GetIfInt); |
| } |
| } // namespace |
| |
| Status ExecuteWindowCommand(const WindowCommand& command, |
| Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| Timeout timeout; |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| status = web_view->HandleReceivedEvents(); |
| if (status.IsError()) |
| return status; |
| |
| if (web_view->IsDialogOpen()) { |
| std::string alert_text; |
| status = web_view->GetDialogMessage(alert_text); |
| if (status.IsError()) |
| return status; |
| |
| std::string dialog_type; |
| status = web_view->GetTypeOfDialog(dialog_type); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| PromptHandlerConfiguration prompt_handler_configuration; |
| status = session->unhandled_prompt_behavior.GetConfiguration( |
| dialog_type, prompt_handler_configuration); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| if (prompt_handler_configuration.type == PromptHandlerType::kAccept || |
| prompt_handler_configuration.type == PromptHandlerType::kDismiss) { |
| status = web_view->HandleDialog( |
| prompt_handler_configuration.type == PromptHandlerType::kAccept, |
| session->prompt_text); |
| if (status.IsError()) { |
| return status; |
| } |
| } |
| |
| if (prompt_handler_configuration.notify) { |
| return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}"); |
| } |
| } |
| |
| Status nav_status(kOk); |
| for (int attempt = 0; attempt < 3; attempt++) { |
| if (attempt == 2) { |
| // Switch to main frame and retry command if subframe no longer exists. |
| session->SwitchToTopFrame(); |
| } |
| |
| nav_status = web_view->WaitForPendingNavigations( |
| session->GetCurrentFrameId(), |
| Timeout(session->page_load_timeout, &timeout), true); |
| // Impossible errors: |
| // * kNoSuchExecutionContext as WebView::WaitForPendingNavigations never |
| // returns it. |
| // Some possible errors: |
| // * kTimeout. The pending navigation has taken too long, the whole command |
| // has timed out. |
| // * kDisconnected. The connection was lost. There is no point to retry. |
| if (nav_status.IsError()) { |
| return nav_status; |
| } |
| |
| status = command.Run(session, web_view, params, value, &timeout); |
| if (kNavigationHints.contains(status.code())) { |
| // Navigation was detected while running the command. Retry. |
| continue; |
| } |
| if (status.code() == kTimeout) { |
| // If the command timed out, let WaitForPendingNavigations cancel |
| // the navigation if there is any. |
| continue; |
| } else if (status.code() == kUnknownError && web_view->IsNonBlocking() && |
| status.message().find(kTargetClosedMessage) != |
| std::string::npos) { |
| // When pageload strategy is None, new navigation can occur during |
| // execution of a command. Retry the command. |
| continue; |
| } else if (status.code() == kDisconnected || |
| status.code() == kTargetDetached) { |
| // Some commands, like clicking a button or link which closes the window, |
| // may result in a kDisconnected or kTargetDetached error code. |
| // |web_view| may be invalid at this point. |
| return status; |
| } else if (status.IsError()) { |
| // If the command failed while a new page or frame started loading, retry |
| // the command after the pending navigation has completed. |
| bool is_pending = false; |
| nav_status = web_view->IsPendingNavigation(&timeout, &is_pending); |
| if (nav_status.IsError()) |
| return nav_status; |
| else if (is_pending) |
| continue; |
| } |
| break; |
| } |
| |
| nav_status = web_view->WaitForPendingNavigations( |
| session->GetCurrentFrameId(), |
| Timeout(session->page_load_timeout, &timeout), true); |
| |
| if (status.IsOk() && nav_status.IsError() && |
| nav_status.code() != kUnexpectedAlertOpen) { |
| return nav_status; |
| } |
| if (status.code() == kUnexpectedAlertOpen) { |
| return Status(kOk); |
| } |
| if (status.code() == kUnexpectedAlertOpen_Keep) { |
| return Status(kUnexpectedAlertOpen, status.message()); |
| } |
| if (kNavigationHints.contains(status.code())) { |
| // The command has failed to run due to pending navigation three times. |
| // Returning a "timeout" error because infinite retries would, presumably, |
| // never end. |
| return Status{kTimeout, status}; |
| } |
| return status; |
| } |
| |
| Status ExecuteGet(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| const std::string* url = params.FindString("url"); |
| if (!url) |
| return Status(kInvalidArgument, "'url' must be a string"); |
| Status status = web_view->Load(*url, timeout); |
| if (status.IsError()) |
| return status; |
| session->SwitchToTopFrame(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteExecuteScript(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* maybe_script = params.FindString("script"); |
| if (!maybe_script) |
| return Status(kInvalidArgument, "'script' must be a string"); |
| std::string script = *maybe_script; |
| if (script == ":takeHeapSnapshot") |
| return web_view->TakeHeapSnapshot(value); |
| if (script == ":startProfile") |
| return web_view->StartProfile(); |
| if (script == ":endProfile") |
| return web_view->EndProfile(value); |
| |
| const base::Value::List* args = params.FindList("args"); |
| if (args == nullptr) { |
| return Status(kInvalidArgument, "'args' must be a list"); |
| } |
| // Need to support line oriented comment |
| if (script.find("//") != std::string::npos) |
| script = script + "\n"; |
| |
| Status status = |
| web_view->CallUserSyncScript(session->GetCurrentFrameId(), script, *args, |
| session->script_timeout, value); |
| switch (status.code()) { |
| case kTimeout: |
| // If the target has been detached the script will never return |
| case kTargetDetached: |
| return Status(kScriptTimeout); |
| default: |
| return status; |
| } |
| } |
| |
| Status ExecuteExecuteAsyncScript(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* maybe_script = params.FindString("script"); |
| if (!maybe_script) |
| return Status(kInvalidArgument, "'script' must be a string"); |
| std::string script = *maybe_script; |
| const base::Value::List* args = params.FindList("args"); |
| if (args == nullptr) { |
| return Status(kInvalidArgument, "'args' must be a list"); |
| } |
| |
| // Need to support line oriented comment |
| if (script.find("//") != std::string::npos) |
| script = script + "\n"; |
| |
| Status status = web_view->CallUserAsyncFunction( |
| session->GetCurrentFrameId(), "async function(){" + script + "}", *args, |
| session->script_timeout, value); |
| switch (status.code()) { |
| case kTimeout: |
| // Navigation has happened during script execution. Further wait would lead |
| // to timeout. |
| case kAbortedByNavigation: |
| return Status(kScriptTimeout); |
| default: |
| return status; |
| } |
| } |
| |
| Status ExecuteNewWindow(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::string type; |
| // "type" can either be None or a string. |
| auto* type_param = params.Find("type"); |
| if (!type_param || type_param->is_none()) { |
| // Nothing more to do |
| } else if (type_param->is_string()) { |
| type = type_param->GetString(); |
| } else { |
| return Status(kInvalidArgument, "missing or invalid 'type'"); |
| } |
| |
| // By default, creates new tab. |
| Chrome::WindowType window_type = (type == "window") |
| ? Chrome::WindowType::kWindow |
| : Chrome::WindowType::kTab; |
| |
| std::string handle; |
| Status status = session->chrome->NewWindow(session->window, window_type, true, |
| session->w3c_compliant, &handle); |
| |
| if (status.IsError()) |
| return status; |
| |
| base::Value::Dict dict; |
| dict.Set("handle", handle); |
| dict.Set("type", |
| (window_type == Chrome::WindowType::kWindow) ? "window" : "tab"); |
| auto results = std::make_unique<base::Value>(std::move(dict)); |
| *value = std::move(results); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSwitchToFrame(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const base::Value* id = params.Find("id"); |
| if (id == nullptr) |
| return Status(kInvalidArgument, "missing 'id'"); |
| |
| if (id->is_none()) { |
| session->SwitchToTopFrame(); |
| return Status(kOk); |
| } |
| |
| std::string script; |
| base::Value::List args; |
| const base::Value::Dict* id_dict = id->GetIfDict(); |
| if (id_dict) { |
| const std::string* element_id = |
| id_dict->FindString(GetElementKey(session->w3c_compliant)); |
| if (!element_id) |
| return Status(kInvalidArgument, "missing 'ELEMENT'"); |
| bool is_displayed = false; |
| Status status = |
| IsElementDisplayed(session, web_view, *element_id, true, &is_displayed); |
| if (status.IsError()) |
| return status; |
| script = "function(elem) { return elem; }"; |
| args.Append(id_dict->Clone()); |
| } else { |
| script = |
| "function(xpath) {" |
| " return document.evaluate(xpath, document, null, " |
| " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;" |
| "}"; |
| std::string xpath = "(/html/body//iframe|/html/frameset//frame)"; |
| if (id->is_string()) { |
| std::string id_string = id->GetString(); |
| if (session->w3c_compliant) |
| return Status(kInvalidArgument, "'id' can not be string"); |
| else |
| xpath += base::StringPrintf( |
| "[@name=\"%s\" or @id=\"%s\"]", id_string.c_str(), id_string.c_str()); |
| } else if (id->is_int()) { |
| int id_int = id->GetInt(); |
| const int max_range = 65535; // 2^16 - 1 |
| if (id_int < 0 || id_int > max_range) |
| return Status(kInvalidArgument, "'id' out of range"); |
| else |
| xpath += base::StringPrintf("[%d]", id_int + 1); |
| } else { |
| return Status(kInvalidArgument, "invalid 'id'"); |
| } |
| args.Append(xpath); |
| } |
| std::string frame; |
| Status status = web_view->GetFrameByFunction( |
| session->GetCurrentFrameId(), script, args, &frame); |
| if (status.IsError()) |
| return status; |
| |
| std::unique_ptr<base::Value> result; |
| status = web_view->CallFunction( |
| session->GetCurrentFrameId(), script, args, &result); |
| if (status.IsError()) |
| return status; |
| const base::Value::Dict* element = result->GetIfDict(); |
| if (!element) |
| return Status(kUnknownError, "fail to locate the sub frame element"); |
| |
| std::string chrome_driver_id = GenerateId(); |
| const char kSetFrameIdentifier[] = |
| "function(frame, id) {" |
| " frame.setAttribute('cd_frame_id_', id);" |
| "}"; |
| base::Value::List new_args; |
| new_args.Append(element->Clone()); |
| new_args.Append(chrome_driver_id); |
| result.reset(); |
| status = web_view->CallFunction( |
| session->GetCurrentFrameId(), kSetFrameIdentifier, new_args, &result); |
| if (status.IsError()) |
| return status; |
| session->SwitchToSubFrame(frame, chrome_driver_id); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSwitchToParentFrame(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| session->SwitchToParentFrame(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetTitle(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const char kGetTitleScript[] = "function() { return document.title;}"; |
| base::Value::List args; |
| return web_view->CallFunction(std::string(), kGetTitleScript, args, value); |
| } |
| |
| Status ExecuteGetPageSource(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const char kGetPageSource[] = |
| "(document.documentElement || {}).outerHTML || ''"; |
| |
| base::Value::List args; |
| return web_view->EvaluateScript(session->GetCurrentFrameId(), kGetPageSource, |
| true, value); |
| } |
| |
| Status ExecuteFindElement(int interval_ms, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return FindElement(interval_ms, true, nullptr, session, web_view, params, |
| value); |
| } |
| |
| Status ExecuteFindElements(int interval_ms, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return FindElement(interval_ms, false, nullptr, session, web_view, params, |
| value); |
| } |
| |
| Status ExecuteGetCurrentUrl(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::string url; |
| Status status = GetUrl(web_view, std::string(), &url); |
| if (status.IsError()) |
| return status; |
| if (url == kUnreachableWebDataURL || |
| url == kDeprecatedUnreachableWebDataURL) { |
| status = web_view->GetUrl(&url); |
| if (status.IsError()) |
| return status; |
| } |
| *value = std::make_unique<base::Value>(url); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGoBack(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| Status status = web_view->TraverseHistory(-1, timeout); |
| if (status.IsError()) |
| return status; |
| session->SwitchToTopFrame(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGoForward(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| Status status = web_view->TraverseHistory(1, timeout); |
| if (status.IsError()) |
| return status; |
| session->SwitchToTopFrame(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteRefresh(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| Status status = web_view->Reload(timeout); |
| if (status.IsError()) |
| return status; |
| session->SwitchToTopFrame(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteFreeze(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| Status status = web_view->Freeze(timeout); |
| return status; |
| } |
| |
| Status ExecuteResume(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| timeout->SetDuration(session->page_load_timeout); |
| Status status = web_view->Resume(timeout); |
| if (status.IsError()) |
| return status; |
| return Status(kOk); |
| } |
| |
| Status ExecuteMouseMoveTo(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::string element_id; |
| bool has_element = false; |
| const std::string* maybe_element_id = params.FindString("element"); |
| if (maybe_element_id) { |
| element_id = *maybe_element_id; |
| has_element = true; |
| } |
| std::optional<int> x_offset = params.FindInt("xoffset"); |
| std::optional<int> y_offset = params.FindInt("yoffset"); |
| bool has_offset = x_offset.has_value() && y_offset.has_value(); |
| if (!has_element && !has_offset) |
| return Status(kInvalidArgument, |
| "at least an element or offset should be set"); |
| |
| WebPoint location; |
| if (has_element) { |
| WebPoint offset; |
| if (has_offset) |
| offset.Offset(*x_offset, *y_offset); |
| Status status = ScrollElementIntoView(session, web_view, element_id, |
| has_offset ? &offset : nullptr, &location); |
| if (status.IsError()) |
| return status; |
| } else { |
| location = session->mouse_position; |
| if (has_offset) |
| location.Offset(*x_offset, *y_offset); |
| } |
| |
| std::vector<MouseEvent> events; |
| events.push_back(MouseEvent(kMovedMouseEventType, |
| session->pressed_mouse_button, location.x, |
| location.y, session->sticky_modifiers, 0, 0)); |
| Status status = web_view->DispatchMouseEvents( |
| events, session->GetCurrentFrameId(), false); |
| if (status.IsOk()) |
| session->mouse_position = location; |
| return status; |
| } |
| |
| Status ExecuteMouseClick(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| MouseButton button; |
| Status status = GetMouseButton(params, &button); |
| if (status.IsError()) |
| return status; |
| std::vector<MouseEvent> events; |
| events.push_back( |
| MouseEvent(kPressedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, 0, 1)); |
| events.push_back( |
| MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, |
| MouseButtonToButtons(button), 1)); |
| session->pressed_mouse_button = kNoneMouseButton; |
| return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(), |
| false); |
| } |
| |
| Status ExecuteMouseButtonDown(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| MouseButton button; |
| Status status = GetMouseButton(params, &button); |
| if (status.IsError()) |
| return status; |
| std::vector<MouseEvent> events; |
| events.push_back( |
| MouseEvent(kPressedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, 0, 1)); |
| session->pressed_mouse_button = button; |
| return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(), |
| false); |
| } |
| |
| Status ExecuteMouseButtonUp(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| MouseButton button; |
| Status status = GetMouseButton(params, &button); |
| if (status.IsError()) |
| return status; |
| std::vector<MouseEvent> events; |
| events.push_back( |
| MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, |
| MouseButtonToButtons(button), 1)); |
| session->pressed_mouse_button = kNoneMouseButton; |
| return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(), |
| false); |
| } |
| |
| Status ExecuteMouseDoubleClick(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| MouseButton button; |
| Status status = GetMouseButton(params, &button); |
| if (status.IsError()) |
| return status; |
| std::vector<MouseEvent> events; |
| events.push_back( |
| MouseEvent(kPressedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, 0, 1)); |
| events.push_back( |
| MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, |
| MouseButtonToButtons(button), 1)); |
| events.push_back( |
| MouseEvent(kPressedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, 0, 2)); |
| events.push_back( |
| MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x, |
| session->mouse_position.y, session->sticky_modifiers, |
| MouseButtonToButtons(button), 2)); |
| session->pressed_mouse_button = kNoneMouseButton; |
| return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(), |
| false); |
| } |
| |
| Status ExecuteTouchDown(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return ExecuteTouchEvent(session, web_view, kTouchStart, params); |
| } |
| |
| Status ExecuteTouchUp(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return ExecuteTouchEvent(session, web_view, kTouchEnd, params); |
| } |
| |
| Status ExecuteTouchMove(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return ExecuteTouchEvent(session, web_view, kTouchMove, params); |
| } |
| |
| Status ExecuteTouchScroll(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| WebPoint location = session->mouse_position; |
| const std::string* element = params.FindString("element"); |
| if (element) { |
| Status status = |
| GetElementClickableLocation(session, web_view, *element, &location); |
| if (status.IsError()) |
| return status; |
| } |
| std::optional<int> xoffset = params.FindInt("xoffset"); |
| if (!xoffset) |
| return Status(kInvalidArgument, "'xoffset' must be an integer"); |
| std::optional<int> yoffset = params.FindInt("yoffset"); |
| if (!yoffset) |
| return Status(kInvalidArgument, "'yoffset' must be an integer"); |
| return web_view->SynthesizeScrollGesture(location.x, location.y, *xoffset, |
| *yoffset); |
| } |
| |
| Status ProcessInputActionSequence(Session* session, |
| const base::Value::Dict& action_sequence, |
| std::vector<base::Value::Dict>* action_list) { |
| const std::string* maybe_type = action_sequence.FindString("type"); |
| std::string pointer_type; |
| if (!maybe_type || ((*maybe_type != "key") && (*maybe_type != "pointer") && |
| (*maybe_type != "wheel") && (*maybe_type != "none"))) { |
| return Status(kInvalidArgument, |
| "'type' must be one of the strings 'key', 'pointer', 'wheel' " |
| "or 'none'"); |
| } |
| const std::string& type = *maybe_type; |
| |
| const std::string* maybe_id = action_sequence.FindString("id"); |
| if (!maybe_id) |
| return Status(kInvalidArgument, "'id' must be a string"); |
| const std::string& id = *maybe_id; |
| |
| if (type == "pointer") { |
| const base::Value::Dict* parameters = |
| action_sequence.FindDict("parameters"); |
| if (parameters) { |
| const std::string* maybe_pointer_type = |
| parameters->FindString("pointerType"); |
| // error check arguments |
| if (!maybe_pointer_type || |
| (*maybe_pointer_type != "mouse" && *maybe_pointer_type != "pen" && |
| *maybe_pointer_type != "touch")) { |
| return Status( |
| kInvalidArgument, |
| "'pointerType' must be a string and one of mouse, pen or touch"); |
| } |
| pointer_type = *maybe_pointer_type; |
| } else { |
| pointer_type = "mouse"; |
| } |
| } |
| |
| bool found = false; |
| for (const base::Value& source_value : session->active_input_sources) { |
| DCHECK(source_value.is_dict()); |
| const base::Value::Dict& source = source_value.GetDict(); |
| |
| std::string source_id; |
| std::string source_type; |
| const std::string* maybe_source_id = source.FindString("id"); |
| const std::string* maybe_source_type = source.FindString("type"); |
| if (maybe_source_id) |
| source_id = *maybe_source_id; |
| if (maybe_source_type) |
| source_type = *maybe_source_type; |
| if (source_id == id && source_type == type) { |
| found = true; |
| if (type == "pointer") { |
| const std::string* source_pointer_type = |
| source.FindString("pointerType"); |
| if (!source_pointer_type || pointer_type != *source_pointer_type) { |
| return Status(kInvalidArgument, |
| "'pointerType' must be a string that matches sources " |
| "pointer type"); |
| } |
| } |
| break; |
| } |
| } |
| |
| // if we found no matching active input source |
| if (!found) { |
| base::Value::Dict tmp_source; |
| // create input source |
| tmp_source.Set("id", id); |
| tmp_source.Set("type", type); |
| if (type == "pointer") { |
| tmp_source.Set("pointerType", pointer_type); |
| } |
| |
| session->active_input_sources.Append(std::move(tmp_source)); |
| |
| base::Value::Dict tmp_state; |
| tmp_state.Set("id", id); |
| if (type == "key") { |
| // Initialize a key input state object |
| // (https://w3c.github.io/webdriver/#dfn-key-input-state). |
| tmp_state.Set("pressed", base::Value::Dict()); |
| // For convenience, we use one integer property to encode four Boolean |
| // properties (alt, shift, ctrl, meta) from the spec, using values from |
| // enum KeyModifierMask. |
| tmp_state.Set("modifiers", 0); |
| } else if (type == "pointer") { |
| int x = 0; |
| int y = 0; |
| |
| // "pressed" is stored as a bitmask of pointer buttons. |
| tmp_state.Set("pressed", 0); |
| tmp_state.Set("subtype", pointer_type); |
| |
| tmp_state.Set("x", x); |
| tmp_state.Set("y", y); |
| } |
| session->input_state_table.SetByDottedPath(id, std::move(tmp_state)); |
| } |
| |
| const base::Value::List* actions = action_sequence.FindList("actions"); |
| if (actions == nullptr) { |
| return Status(kInvalidArgument, "'actions' in the sequence must be a list"); |
| } |
| |
| std::unique_ptr<base::Value::List> actions_result(new base::Value::List); |
| for (const base::Value& action_item_value : *actions) { |
| base::Value::Dict action_dict; |
| |
| if (!action_item_value.is_dict()) { |
| return Status( |
| kInvalidArgument, |
| "each argument in the action sequence must be a dictionary"); |
| } |
| |
| const base::Value::Dict& action_item = action_item_value.GetDict(); |
| |
| action_dict.Set("id", id); |
| action_dict.Set("type", type); |
| |
| if (type == "none") { |
| // process none action |
| const std::string* subtype = action_item.FindString("type"); |
| if (!subtype || *subtype != "pause") |
| return Status(kInvalidArgument, |
| "type of action must be the string 'pause'"); |
| |
| action_dict.Set("subtype", *subtype); |
| |
| Status status = ProcessPauseAction(action_item, &action_dict); |
| if (status.IsError()) |
| return status; |
| } else if (type == "key") { |
| // process key action |
| const std::string* subtype = action_item.FindString("type"); |
| if (!subtype || |
| (*subtype != "keyUp" && *subtype != "keyDown" && *subtype != "pause")) |
| return Status( |
| kInvalidArgument, |
| "type of action must be the string 'keyUp', 'keyDown' or 'pause'"); |
| |
| action_dict.Set("subtype", *subtype); |
| if (*subtype == "pause") { |
| Status status = ProcessPauseAction(action_item, &action_dict); |
| if (status.IsError()) |
| return status; |
| } else { |
| const std::string* key = action_item.FindString("value"); |
| bool valid = (key != nullptr); |
| if (valid) { |
| // check if key is a single unicode code point |
| size_t char_index = 0; |
| base_icu::UChar32 code_point; |
| valid = base::ReadUnicodeCharacter(key->c_str(), key->size(), |
| &char_index, &code_point) && |
| char_index + 1 == key->size(); |
| } |
| if (!valid) |
| return Status(kInvalidArgument, |
| "'value' must be a single Unicode code point"); |
| action_dict.Set("value", *key); |
| } |
| } else if (type == "pointer" || type == "wheel") { |
| const std::string* subtype = action_item.FindString("type"); |
| if (type == "pointer") { |
| if (!subtype || (*subtype != "pointerUp" && *subtype != "pointerDown" && |
| *subtype != "pointerMove" && |
| *subtype != "pointerCancel" && *subtype != "pause")) { |
| return Status(kInvalidArgument, |
| "type of pointer action must be the string " |
| "'pointerUp', 'pointerDown', 'pointerMove' or " |
| "'pause'"); |
| } |
| } else { |
| if (!subtype || (*subtype != "scroll" && *subtype != "pause")) { |
| return Status( |
| kInvalidArgument, |
| "type of action must be the string 'scroll' or 'pause'"); |
| } |
| } |
| |
| action_dict.Set("subtype", *subtype); |
| action_dict.Set("pointerType", pointer_type); |
| |
| if (*subtype == "pointerDown" || *subtype == "pointerUp") { |
| if (pointer_type == "mouse" || pointer_type == "pen") { |
| int button = action_item.FindInt("button").value_or(-1); |
| if (button < 0 || button > 4) { |
| return Status( |
| kInvalidArgument, |
| "'button' must be a non-negative int and between 0 and 4"); |
| } |
| std::string button_str; |
| Status status = IntToStringButton(button, button_str); |
| if (status.IsError()) |
| return status; |
| action_dict.Set("button", button_str); |
| } |
| } else if (*subtype == "pointerMove" || *subtype == "scroll") { |
| if (*subtype == "scroll") { |
| std::optional<int> x = action_item.FindInt("x"); |
| if (!x.has_value()) { |
| return Status(kInvalidArgument, "'x' must be an int"); |
| } |
| std::optional<int> y = action_item.FindInt("y"); |
| if (!y.has_value()) { |
| return Status(kInvalidArgument, "'y' must be an int"); |
| } |
| action_dict.Set("x", *x); |
| action_dict.Set("y", *y); |
| } else { |
| std::optional<double> x = action_item.FindDouble("x"); |
| if (!x.has_value()) { |
| return Status(kInvalidArgument, "'x' must be a number"); |
| } |
| std::optional<double> y = action_item.FindDouble("y"); |
| if (!y.has_value()) { |
| return Status(kInvalidArgument, "'y' must be a number"); |
| } |
| action_dict.Set("x", *x); |
| action_dict.Set("y", *y); |
| } |
| |
| const base::Value* origin_val = action_item.Find("origin"); |
| if (origin_val) { |
| if (!origin_val->is_string()) { |
| const base::Value::Dict* origin_dict = origin_val->GetIfDict(); |
| if (!origin_dict) |
| return Status(kInvalidArgument, |
| "'origin' must be either a string or a dictionary"); |
| const std::string* element_id = |
| origin_dict->FindString(GetElementKey(session->w3c_compliant)); |
| if (!element_id) |
| return Status(kInvalidArgument, "'element' is missing"); |
| base::Value* origin_result = |
| action_dict.Set("origin", base::Value(base::Value::Type::DICT)); |
| origin_result->GetDict().SetByDottedPath( |
| GetElementKey(session->w3c_compliant), *element_id); |
| } else { |
| const std::string& origin = origin_val->GetString(); |
| if (origin != "viewport" && origin != "pointer") |
| return Status(kInvalidArgument, |
| "if 'origin' is a string, it must be either " |
| "'viewport' or 'pointer'"); |
| action_dict.Set("origin", origin); |
| } |
| } else { |
| action_dict.Set("origin", "viewport"); |
| } |
| |
| Status status = ProcessPauseAction(action_item, &action_dict); |
| if (status.IsError()) |
| return status; |
| |
| if (*subtype == "scroll") { |
| std::optional<int> delta_x = action_item.FindInt("deltaX"); |
| if (!delta_x) |
| return Status(kInvalidArgument, "'delta x' must be an int"); |
| std::optional<int> delta_y = action_item.FindInt("deltaY"); |
| if (!delta_y) |
| return Status(kInvalidArgument, "'delta y' must be an int"); |
| action_dict.Set("deltaX", *delta_x); |
| action_dict.Set("deltaY", *delta_y); |
| } |
| } else if (*subtype == "pause") { |
| Status status = ProcessPauseAction(action_item, &action_dict); |
| if (status.IsError()) |
| return status; |
| } |
| |
| // Process Pointer Event's properties. |
| std::optional<double> maybe_double_value; |
| std::optional<int> maybe_int_value; |
| |
| maybe_double_value = ParseDoubleIfInDictionary(action_item, "width", 1); |
| if (!maybe_double_value.has_value() || maybe_double_value.value() < 0) |
| return Status(kInvalidArgument, |
| "'width' must be a non-negative number"); |
| action_dict.Set("width", maybe_double_value.value()); |
| |
| maybe_double_value = ParseDoubleIfInDictionary(action_item, "height", 1); |
| if (!maybe_double_value.has_value() || maybe_double_value.value() < 0) |
| return Status(kInvalidArgument, |
| "'height' must be a non-negative number"); |
| action_dict.Set("height", maybe_double_value.value()); |
| |
| maybe_double_value = |
| ParseDoubleIfInDictionary(action_item, "pressure", 0.5); |
| if (!maybe_double_value.has_value() || maybe_double_value.value() < 0 || |
| maybe_double_value.value() > 1) |
| return Status( |
| kInvalidArgument, |
| "'pressure' must be a non-negative number in the range of [0,1]"); |
| action_dict.Set("pressure", maybe_double_value.value()); |
| |
| maybe_double_value = |
| ParseDoubleIfInDictionary(action_item, "tangentialPressure", 0); |
| if (!maybe_double_value.has_value() || maybe_double_value.value() < -1 || |
| maybe_double_value.value() > 1) |
| return Status( |
| kInvalidArgument, |
| "'tangentialPressure' must be a number in the range of [-1,1]"); |
| action_dict.Set("tangentialPressure", maybe_double_value.value()); |
| |
| maybe_int_value = ParseIntIfInDictionary(action_item, "tiltX", 0); |
| if (!maybe_int_value.has_value() || maybe_int_value.value() < -90 || |
| maybe_int_value.value() > 90) |
| return Status(kInvalidArgument, |
| "'tiltX' must be an integer in the range of [-90,90]"); |
| action_dict.Set("tiltX", maybe_int_value.value()); |
| |
| maybe_int_value = ParseIntIfInDictionary(action_item, "tiltY", 0); |
| if (!maybe_int_value.has_value() || maybe_int_value.value() < -90 || |
| maybe_int_value.value() > 90) |
| return Status(kInvalidArgument, |
| "'tiltY' must be an integer in the range of [-90,90]"); |
| action_dict.Set("tiltY", maybe_int_value.value()); |
| |
| maybe_int_value = ParseIntIfInDictionary(action_item, "twist", 0); |
| if (!maybe_int_value.has_value() || maybe_int_value.value() < 0 || |
| maybe_int_value.value() > 359) |
| return Status(kInvalidArgument, |
| "'twist' must be an integer in the range of [0,359]"); |
| action_dict.Set("twist", maybe_int_value.value()); |
| } |
| action_list->push_back(std::move(action_dict)); |
| } |
| return Status(kOk); |
| } |
| |
| Status ExecutePerformActions(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| // extract action sequence |
| const base::Value::List* actions_input = params.FindList("actions"); |
| if (actions_input == nullptr) { |
| return Status(kInvalidArgument, "'actions' must be a list"); |
| } |
| |
| // the processed actions |
| std::vector<std::vector<base::Value::Dict>> actions_list; |
| for (const base::Value& action_sequence : *actions_input) { |
| // process input action sequence |
| if (!action_sequence.is_dict()) |
| return Status(kInvalidArgument, "each argument must be a dictionary"); |
| |
| std::vector<base::Value::Dict> action_list; |
| Status status = ProcessInputActionSequence( |
| session, action_sequence.GetDict(), &action_list); |
| actions_list.push_back(std::move(action_list)); |
| |
| if (status.IsError()) |
| return Status(kInvalidArgument, status); |
| } |
| |
| std::set<std::string> pointer_id_set; |
| std::vector<base::Value::Dict*> action_input_states; |
| std::map<std::string, gfx::Point> action_locations; |
| std::map<std::string, bool> has_touch_start; |
| std::map<std::string, int> buttons; |
| std::map<std::string, int> last_pressed_buttons; |
| std::map<std::string, std::string> button_type; |
| int viewport_width = 0, viewport_height = 0; |
| int init_x = 0, init_y = 0; |
| |
| size_t longest_action_list_size = 0; |
| for (size_t i = 0; i < actions_list.size(); i++) { |
| longest_action_list_size = |
| std::max(longest_action_list_size, actions_list[i].size()); |
| } |
| |
| for (size_t i = 0; i < longest_action_list_size; i++) { |
| // Find the last pointer action, and it has to be sent synchronously by |
| // default. |
| size_t last_action_index = 0; |
| size_t last_touch_index = 0; |
| for (size_t j = 0; j < actions_list.size(); j++) { |
| if (actions_list[j].size() > i) { |
| const base::Value::Dict& action = actions_list[j][i]; |
| std::string type; |
| std::string action_type; |
| GetOptionalString(action, "type", &type); |
| GetOptionalString(action, "subtype", &action_type); |
| if (type != "none" && action_type != "pause") |
| last_action_index = j; |
| |
| if (type == "pointer") { |
| std::string pointer_type; |
| GetOptionalString(action, "pointerType", &pointer_type); |
| if (pointer_type == "touch") |
| last_touch_index = j; |
| } |
| } |
| } |
| |
| // Implements "compute the tick duration" algorithm from W3C spec |
| // (https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration). |
| // This is the duration for actions in one tick. |
| int tick_duration = 0; |
| std::vector<TouchEvent> dispatch_touch_events; |
| for (size_t j = 0; j < actions_list.size(); j++) { |
| if (actions_list[j].size() > i) { |
| const base::Value::Dict& action = actions_list[j][i]; |
| std::string id; |
| std::string type; |
| std::string action_type; |
| GetOptionalString(action, "id", &id); |
| |
| base::Value::Dict* input_state = |
| session->input_state_table.FindDictByDottedPath(id); |
| if (!input_state) |
| return Status(kUnknownError, "missing input state"); |
| |
| GetOptionalString(action, "type", &type); |
| if (i == 0) { |
| if (pointer_id_set.find(id) != pointer_id_set.end()) |
| return Status(kInvalidArgument, "'id' already exists"); |
| pointer_id_set.insert(id); |
| action_input_states.push_back(input_state); |
| |
| if (type == "pointer" || type == "wheel") { |
| Status status = WindowViewportSize( |
| session, web_view, &viewport_width, &viewport_height); |
| if (status.IsError()) |
| return status; |
| std::optional<int> maybe_init_x = input_state->FindInt("x"); |
| if (maybe_init_x) |
| init_x = *maybe_init_x; |
| |
| std::optional<int> maybe_init_y = input_state->FindInt("y"); |
| if (maybe_init_y) |
| init_y = *maybe_init_y; |
| action_locations.insert( |
| std::make_pair(id, gfx::Point(init_x, init_y))); |
| |
| std::string pointer_type; |
| GetOptionalString(action, "pointerType", &pointer_type); |
| if (pointer_type == "mouse" || pointer_type == "pen") { |
| buttons[id] = input_state->Find("pressed")->GetInt(); |
| last_pressed_buttons[id] = buttons[id]; |
| } else if (pointer_type == "touch") { |
| has_touch_start[id] = false; |
| } |
| } |
| } |
| |
| GetOptionalString(action, "subtype", &action_type); |
| int duration = 0; |
| if (action_type == "pause") { |
| GetOptionalInt(action, "duration", &duration); |
| tick_duration = std::max(tick_duration, duration); |
| } |
| |
| if (type != "none") { |
| bool async_dispatch_event = true; |
| if (j == last_action_index) { |
| async_dispatch_event = false; |
| GetOptionalBool(action, "asyncDispatch", &async_dispatch_event); |
| } |
| |
| if (type == "key") { |
| if (action_type != "pause") { |
| std::vector<KeyEvent> dispatch_key_events; |
| KeyEventBuilder builder; |
| Status status = ConvertKeyActionToKeyEvent( |
| action, *input_state, action_type == "keyDown", |
| &dispatch_key_events); |
| if (status.IsError()) |
| return status; |
| |
| if (dispatch_key_events.size() > 0) { |
| const KeyEvent& event = dispatch_key_events.front(); |
| if (action_type == "keyDown") { |
| session->input_cancel_list.emplace_back( |
| action_input_states[j], nullptr, nullptr, &event); |
| session->sticky_modifiers |= KeyToKeyModifiers(event.key); |
| } else if (action_type == "keyUp") { |
| session->sticky_modifiers &= ~KeyToKeyModifiers(event.key); |
| } |
| |
| status = web_view->DispatchKeyEvents(dispatch_key_events, |
| async_dispatch_event); |
| if (status.IsError()) |
| return status; |
| } |
| } |
| } else if (type == "pointer" || type == "wheel") { |
| std::string element_id; |
| if (action_type == "pointerMove" || action_type == "scroll") { |
| double x = action.FindDouble("x").value_or(0); |
| double y = action.FindDouble("y").value_or(0); |
| if (const base::Value* origin_val = action.Find("origin")) { |
| if (const base::Value::Dict* origin_dict = |
| origin_val->GetIfDict()) { |
| GetOptionalString(*origin_dict, |
| GetElementKey(session->w3c_compliant), |
| &element_id); |
| if (!element_id.empty()) { |
| int center_x = 0, center_y = 0; |
| Status status = ElementInViewCenter( |
| session, web_view, element_id, ¢er_x, ¢er_y); |
| if (status.IsError()) |
| return status; |
| x += center_x; |
| y += center_y; |
| } |
| } else { |
| std::string origin_str; |
| GetOptionalString(action, "origin", &origin_str); |
| if (origin_str == "pointer") { |
| x += action_locations[id].x(); |
| y += action_locations[id].y(); |
| } |
| } |
| } |
| if (x < 0 || x > viewport_width || y < 0 || y > viewport_height) |
| return Status(kMoveTargetOutOfBounds); |
| |
| action_locations[id] = gfx::Point(x, y); |
| |
| duration = 0; |
| GetOptionalInt(action, "duration", &duration); |
| tick_duration = std::max(tick_duration, duration); |
| |
| if (action_type == "scroll") { |
| int delta_x = action.FindInt("deltaX").value_or(0); |
| int delta_y = action.FindInt("deltaY").value_or(0); |
| std::vector<MouseEvent> dispatch_wheel_events; |
| MouseEvent event(StringToMouseEventType(action_type), |
| StringToMouseButton(button_type[id]), |
| action_locations[id].x(), |
| action_locations[id].y(), 0, buttons[id], 0); |
| event.modifiers = session->sticky_modifiers; |
| event.delta_x = delta_x; |
| event.delta_y = delta_y; |
| buttons[id] |= StringToModifierMouseButton(button_type[id]); |
| last_pressed_buttons[id] = |
| StringToModifierMouseButton(button_type[id]); |
| session->mouse_position = WebPoint(event.x, event.y); |
| dispatch_wheel_events.push_back(event); |
| Status status = web_view->DispatchMouseEvents( |
| dispatch_wheel_events, session->GetCurrentFrameId(), |
| async_dispatch_event); |
| if (status.IsError()) |
| return status; |
| } |
| } |
| |
| double width = action.FindDouble("width").value_or(1); |
| double height = action.FindDouble("height").value_or(1); |
| double pressure = action.FindDouble("pressure").value_or(0.5); |
| double tangential_pressure = |
| action.FindDouble("tangentialPressure").value_or(0); |
| int tilt_x = action.FindInt("tiltX").value_or(0); |
| int tilt_y = action.FindInt("tiltY").value_or(0); |
| int twist = action.FindInt("twist").value_or(0); |
| |
| std::string pointer_type; |
| GetOptionalString(action, "pointerType", &pointer_type); |
| if (pointer_type == "mouse" || pointer_type == "pen") { |
| if (action_type != "pause") { |
| std::vector<MouseEvent> dispatch_mouse_events; |
| int click_count = 0; |
| if (action_type == "pointerDown" || |
| action_type == "pointerUp") { |
| std::string button; |
| GetOptionalString(action, "button", &button); |
| button_type[id] = button; |
| click_count = 1; |
| } else if (buttons[id] == 0) { |
| button_type[id].clear(); |
| } |
| |
| MouseEvent event(StringToMouseEventType(action_type), |
| StringToMouseButton(button_type[id]), |
| action_locations[id].x(), |
| action_locations[id].y(), 0, buttons[id], |
| click_count); |
| event.pointer_type = StringToPointerType(pointer_type); |
| event.modifiers = session->sticky_modifiers; |
| event.tangentialPressure = tangential_pressure; |
| event.tiltX = tilt_x; |
| event.tiltY = tilt_y; |
| event.twist = twist; |
| |
| if (event.type == kPressedMouseEventType) { |
| base::TimeTicks timestamp = base::TimeTicks::Now(); |
| event.click_count = GetMouseClickCount( |
| session->click_count, event.x, event.y, |
| session->mouse_position.x, session->mouse_position.y, |
| StringToModifierMouseButton(button_type[id]), |
| last_pressed_buttons[id], timestamp, |
| session->mouse_click_timestamp); |
| buttons[id] |= StringToModifierMouseButton(button_type[id]); |
| last_pressed_buttons[id] = |
| StringToModifierMouseButton(button_type[id]); |
| session->mouse_position = WebPoint(event.x, event.y); |
| session->click_count = event.click_count; |
| session->mouse_click_timestamp = timestamp; |
| session->input_cancel_list.emplace_back( |
| action_input_states[j], &event, nullptr, nullptr); |
| action_input_states[j]->Set( |
| "pressed", |
| action_input_states[j]->Find("pressed")->GetInt() | |
| (1 << event.button)); |
| } else if (event.type == kReleasedMouseEventType) { |
| pressure = 0; |
| event.click_count = session->click_count; |
| buttons[id] &= ~StringToModifierMouseButton(button_type[id]); |
| action_input_states[j]->Set( |
| "pressed", |
| action_input_states[j]->Find("pressed")->GetInt() & |
| ~(1 << event.button)); |
| } else if (event.type == kMovedMouseEventType) { |
| if (action_input_states[j]->Find("pressed")->GetInt() == 0) { |
| pressure = 0; |
| } |
| } |
| event.force = pressure; |
| dispatch_mouse_events.push_back(event); |
| Status status = web_view->DispatchMouseEvents( |
| dispatch_mouse_events, session->GetCurrentFrameId(), |
| async_dispatch_event); |
| if (status.IsError()) |
| return status; |
| } |
| } else if (pointer_type == "touch") { |
| if (action_type == "pointerDown") |
| has_touch_start[id] = true; |
| TouchEvent event(StringToTouchEventType(action_type), |
| action_locations[id].x(), |
| action_locations[id].y()); |
| event.radiusX = width / 2.f; |
| event.radiusY = height / 2.f; |
| event.force = pressure; |
| event.tangentialPressure = tangential_pressure; |
| event.tiltX = tilt_x; |
| event.tiltY = tilt_y; |
| event.twist = twist; |
| if (event.type == kTouchStart) { |
| session->input_cancel_list.emplace_back( |
| action_input_states[j], nullptr, &event, nullptr); |
| action_input_states[j]->Set("pressed", 1); |
| } else if (event.type == kTouchEnd) { |
| action_input_states[j]->Set("pressed", 0); |
| } |
| if (has_touch_start[id]) { |
| if (event.type == kPause) |
| event.type = kTouchMove; |
| event.id = j; |
| dispatch_touch_events.push_back(event); |
| } |
| if (j == last_touch_index) { |
| Status status = web_view->DispatchTouchEventWithMultiPoints( |
| dispatch_touch_events, async_dispatch_event); |
| if (status.IsError()) |
| return status; |
| } |
| if (action_type == "pointerUp") |
| has_touch_start[id] = false; |
| } |
| action_input_states[j]->Set("x", action_locations[id].x()); |
| action_input_states[j]->Set("y", action_locations[id].y()); |
| } |
| } |
| } |
| } |
| |
| if (tick_duration > 0) { |
| base::PlatformThread::Sleep(base::Milliseconds(tick_duration)); |
| } |
| } |
| |
| return Status(kOk); |
| } |
| |
| Status ExecuteReleaseActions(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| for (const InputCancelListEntry& entry : |
| base::Reversed(session->input_cancel_list)) { |
| if (entry.key_event) { |
| base::Value::Dict* pressed = entry.input_state->FindDict("pressed"); |
| if (!pressed->Find(entry.key_event->key)) |
| continue; |
| web_view->DispatchKeyEvents({*entry.key_event}, false); |
| pressed->Remove(entry.key_event->key); |
| } else if (entry.mouse_event) { |
| int pressed = entry.input_state->Find("pressed")->GetInt(); |
| int button_mask = 1 << entry.mouse_event->button; |
| if ((pressed & button_mask) == 0) |
| continue; |
| web_view->DispatchMouseEvents({*entry.mouse_event}, |
| session->GetCurrentFrameId(), false); |
| entry.input_state->Set("pressed", pressed & ~button_mask); |
| } else if (entry.touch_event) { |
| int pressed = entry.input_state->Find("pressed")->GetInt(); |
| if (pressed == 0) |
| continue; |
| web_view->DispatchTouchEvents({*entry.touch_event}, false); |
| entry.input_state->Set("pressed", 0); |
| } |
| } |
| |
| session->input_cancel_list.clear(); |
| session->input_state_table.clear(); |
| session->active_input_sources.clear(); |
| session->mouse_position = WebPoint(0, 0); |
| session->click_count = 0; |
| session->mouse_click_timestamp = base::TimeTicks::Now(); |
| session->sticky_modifiers = 0; |
| |
| return Status(kOk); |
| } |
| |
| Status ExecuteSendCommand(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* cmd = params.FindString("cmd"); |
| if (!cmd) { |
| return Status(kInvalidArgument, "command not passed"); |
| } |
| const base::Value::Dict* cmd_params = params.FindDict("params"); |
| if (!cmd_params) { |
| return Status(kInvalidArgument, "params not passed"); |
| } |
| return web_view->SendCommand(*cmd, *cmd_params); |
| } |
| |
| Status ExecuteSendCommandFromWebSocket(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* cmd = params.FindString("method"); |
| if (!cmd) { |
| return Status(kInvalidArgument, "command not passed"); |
| } |
| const base::Value::Dict* cmd_params = params.FindDict("params"); |
| if (!cmd_params) { |
| return Status(kInvalidArgument, "params not passed"); |
| } |
| std::optional<int> client_cmd_id = params.FindInt("id"); |
| if (!client_cmd_id || !CommandId::IsClientCommandId(*client_cmd_id)) { |
| return Status(kInvalidArgument, "command id must be negative"); |
| } |
| |
| return web_view->SendCommandFromWebSocket(*cmd, *cmd_params, *client_cmd_id); |
| } |
| |
| Status ExecuteSendCommandAndGetResult(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* cmd = params.FindString("cmd"); |
| if (!cmd) { |
| return Status(kInvalidArgument, "command not passed"); |
| } |
| const base::Value::Dict* cmd_params = params.FindDict("params"); |
| if (!cmd_params) { |
| return Status(kInvalidArgument, "params not passed"); |
| } |
| return web_view->SendCommandAndGetResult(*cmd, *cmd_params, value); |
| } |
| |
| Status ExecuteGetActiveElement(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return GetActiveElement(session, web_view, value); |
| } |
| |
| Status ExecuteSendKeysToActiveElement(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const base::Value::List* key_list = params.FindList("value"); |
| if (key_list == nullptr) { |
| return Status(kInvalidArgument, "'value' must be a list"); |
| } |
| return SendKeysOnWindow( |
| web_view, key_list, false, &session->sticky_modifiers); |
| } |
| |
| Status ExecuteGetStorageItem(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* key = params.FindString("key"); |
| if (!key) |
| return Status(kInvalidArgument, "'key' must be a string"); |
| base::Value::List args; |
| args.Append(*key); |
| return web_view->CallFunction( |
| session->GetCurrentFrameId(), |
| base::StringPrintf("function(key) { return %s[key]; }", storage), |
| std::move(args), value); |
| } |
| |
| Status ExecuteGetStorageKeys(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const char script[] = |
| "var keys = [];" |
| "var storage = %s;" |
| "for (var i = 0; i < storage.length; i++) {" |
| " keys.push(storage.key(i));" |
| "}" |
| "keys"; |
| return web_view->EvaluateScript(session->GetCurrentFrameId(), |
| base::StringPrintf(script, storage), false, |
| value); |
| } |
| |
| Status ExecuteSetStorageItem(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* key = params.FindString("key"); |
| if (!key) |
| return Status(kInvalidArgument, "'key' must be a string"); |
| const std::string* storage_value = params.FindString("value"); |
| if (!storage_value) |
| return Status(kInvalidArgument, "'value' must be a string"); |
| base::Value::List args; |
| args.Append(*key); |
| args.Append(*storage_value); |
| return web_view->CallFunction( |
| session->GetCurrentFrameId(), |
| base::StringPrintf("function(key, value) { %s[key] = value; }", storage), |
| args, |
| value); |
| } |
| |
| Status ExecuteRemoveStorageItem(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* key = params.FindString("key"); |
| if (!key) |
| return Status(kInvalidArgument, "'key' must be a string"); |
| base::Value::List args; |
| args.Append(*key); |
| return web_view->CallFunction( |
| session->GetCurrentFrameId(), |
| base::StringPrintf("function(key) { %s.removeItem(key) }", storage), |
| args, |
| value); |
| } |
| |
| Status ExecuteClearStorage(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->EvaluateScript(session->GetCurrentFrameId(), |
| base::StringPrintf("%s.clear()", storage), |
| false, value); |
| } |
| |
| Status ExecuteGetStorageSize(const char* storage, |
| Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->EvaluateScript(session->GetCurrentFrameId(), |
| base::StringPrintf("%s.length", storage), |
| false, value); |
| } |
| |
| Status ExecuteScreenshot(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Status status = session->chrome->ActivateWebView(web_view->GetId()); |
| if (status.IsError()) |
| return status; |
| |
| std::string screenshot; |
| status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict()); |
| if (status.IsError()) { |
| if (status.code() == kUnexpectedAlertOpen) { |
| LOG(WARNING) << status.message() << ", cancelling screenshot"; |
| // we can't take screenshot in this state |
| // but we must return kUnexpectedAlertOpen_Keep instead |
| // see https://crbug.com/chromedriver/2117 |
| return Status(kUnexpectedAlertOpen_Keep); |
| } |
| LOG(WARNING) << "screenshot failed, retrying " << status.message(); |
| status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict()); |
| } |
| if (status.IsError()) |
| return status; |
| |
| *value = std::make_unique<base::Value>(screenshot); |
| return Status(kOk); |
| } |
| |
| Status ExecuteFullPageScreenshot(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Status status = session->chrome->ActivateWebView(web_view->GetId()); |
| if (status.IsError()) |
| return status; |
| |
| std::unique_ptr<base::Value> layout_metrics; |
| // TODO(crbug.com/40911917): Pass base::Value::Dict* as return param. |
| status = web_view->SendCommandAndGetResult( |
| "Page.getLayoutMetrics", base::Value::Dict(), &layout_metrics); |
| if (status.IsError()) |
| return status; |
| |
| CHECK(layout_metrics && layout_metrics->is_dict()); |
| const auto& layout_metrics_dict = layout_metrics->GetDict(); |
| const auto width = |
| layout_metrics_dict.FindDoubleByDottedPath("contentSize.width"); |
| if (!width.has_value()) |
| return Status(kUnknownError, "invalid width type"); |
| int w = ceil(width.value()); |
| if (w == 0) |
| return Status(kUnknownError, "invalid width 0"); |
| |
| const auto height = |
| layout_metrics_dict.FindDoubleByDottedPath("contentSize.height"); |
| if (!height.has_value()) |
| return Status(kUnknownError, "invalid height type"); |
| int h = ceil(height.value()); |
| if (h == 0) |
| return Status(kUnknownError, "invalid height 0"); |
| |
| auto* meom = web_view->GetMobileEmulationOverrideManager(); |
| bool has_override_metrics = meom->HasOverrideMetrics(); |
| |
| base::Value::Dict device_metrics; |
| device_metrics.Set("width", w); |
| device_metrics.Set("height", h); |
| if (has_override_metrics) { |
| const auto* dm = meom->GetDeviceMetrics(); |
| device_metrics.Set("deviceScaleFactor", dm->device_scale_factor); |
| device_metrics.Set("mobile", dm->mobile); |
| } else { |
| device_metrics.Set("deviceScaleFactor", 1); |
| device_metrics.Set("mobile", false); |
| } |
| std::unique_ptr<base::Value> ignore; |
| status = web_view->SendCommandAndGetResult( |
| "Emulation.setDeviceMetricsOverride", device_metrics, &ignore); |
| if (status.IsError()) |
| return status; |
| |
| std::string screenshot; |
| // No need to supply clip as it would be default to the device metrics |
| // parameters |
| status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict()); |
| if (status.IsError()) { |
| if (status.code() == kUnexpectedAlertOpen) { |
| LOG(WARNING) << status.message() << ", cancelling screenshot"; |
| // we can't take screenshot in this state |
| // but we must return kUnexpectedAlertOpen_Keep instead |
| // see https://crbug.com/chromedriver/2117 |
| return Status(kUnexpectedAlertOpen_Keep); |
| } |
| LOG(WARNING) << "screenshot failed, retrying " << status.message(); |
| status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict()); |
| } |
| if (status.IsError()) |
| return status; |
| |
| *value = std::make_unique<base::Value>(screenshot); |
| |
| // Check if there is already deviceMetricsOverride in use, |
| // if so, restore to that instead |
| if (has_override_metrics) { |
| status = meom->RestoreOverrideMetrics(); |
| } else { |
| // The scroll bar disappear after setting device metrics to fullpage |
| // width and height, this is to clear device metrics and restore |
| // scroll bars |
| status = web_view->SendCommandAndGetResult( |
| "Emulation.clearDeviceMetricsOverride", base::Value::Dict(), &ignore); |
| } |
| return status; |
| } |
| |
| Status ExecutePrint(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::string orientation; |
| Status status = ParseOrientation(params, &orientation); |
| if (status.IsError()) |
| return status; |
| |
| double scale; |
| status = ParseScale(params, &scale); |
| if (status.IsError()) |
| return status; |
| |
| bool background; |
| status = ParseBoolean(params, "background", false, &background); |
| if (status.IsError()) |
| return status; |
| |
| Page page; |
| status = ParsePage(params, &page); |
| if (status.IsError()) |
| return status; |
| |
| Margin margin; |
| status = ParseMargin(params, &margin); |
| if (status.IsError()) |
| return status; |
| |
| bool shrink_to_fit; |
| status = ParseBoolean(params, "shrinkToFit", true, &shrink_to_fit); |
| if (status.IsError()) |
| return status; |
| |
| std::string page_ranges; |
| status = ParsePageRanges(params, &page_ranges); |
| if (status.IsError()) |
| return status; |
| |
| base::Value::Dict print_params; |
| print_params.Set(kLandscape, orientation == kLandscape); |
| print_params.Set("scale", scale); |
| print_params.Set("printBackground", background); |
| print_params.Set("paperWidth", page.width); |
| print_params.Set("paperHeight", page.height); |
| print_params.Set("marginTop", margin.top); |
| print_params.Set("marginBottom", margin.bottom); |
| print_params.Set("marginLeft", margin.left); |
| print_params.Set("marginRight", margin.right); |
| print_params.Set("preferCSSPageSize", !shrink_to_fit); |
| print_params.Set("pageRanges", page_ranges); |
| print_params.Set("transferMode", "ReturnAsBase64"); |
| |
| std::string pdf; |
| status = web_view->PrintToPDF(print_params, &pdf); |
| if (status.IsError()) |
| return status; |
| |
| *value = std::make_unique<base::Value>(pdf); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetCookies(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::list<Cookie> cookies; |
| Status status = GetVisibleCookies(session, web_view, &cookies); |
| if (status.IsError()) |
| return status; |
| auto cookie_list = std::make_unique<base::Value>(base::Value::Type::LIST); |
| for (std::list<Cookie>::const_iterator it = cookies.begin(); |
| it != cookies.end(); ++it) { |
| cookie_list->GetList().Append(CreateDictionaryFrom(*it)); |
| } |
| *value = std::move(cookie_list); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetNamedCookie(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* name = params.FindString("name"); |
| if (!name) |
| return Status(kInvalidArgument, "missing 'cookie name'"); |
| |
| std::list<Cookie> cookies; |
| Status status = GetVisibleCookies(session, web_view, &cookies); |
| if (status.IsError()) |
| return status; |
| |
| for (std::list<Cookie>::const_iterator it = cookies.begin(); |
| it != cookies.end(); ++it) { |
| if (*name == it->name) { |
| *value = |
| base::Value::ToUniquePtrValue(base::Value(CreateDictionaryFrom(*it))); |
| return Status(kOk); |
| } |
| } |
| return Status(kNoSuchCookie); |
| } |
| |
| Status ExecuteAddCookie(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const base::Value::Dict* cookie = params.FindDict("cookie"); |
| if (!cookie) |
| return Status(kInvalidArgument, "missing 'cookie'"); |
| const std::string* name = cookie->FindString("name"); |
| const std::string* cookie_value = cookie->FindString("value"); |
| if (!name) |
| return Status(kInvalidArgument, "missing 'name'"); |
| if (!cookie_value) |
| return Status(kInvalidArgument, "missing 'value'"); |
| std::string url; |
| Status status = GetUrl(web_view, session->GetCurrentFrameId(), &url); |
| if (status.IsError()) |
| return status; |
| if (!base::StartsWith(url, "http://", base::CompareCase::INSENSITIVE_ASCII) && |
| !base::StartsWith(url, "https://", |
| base::CompareCase::INSENSITIVE_ASCII) && |
| !base::StartsWith(url, "ftp://", base::CompareCase::INSENSITIVE_ASCII)) |
| return Status(kInvalidCookieDomain); |
| std::string domain; |
| if (!GetOptionalString(*cookie, "domain", &domain)) |
| return Status(kInvalidArgument, "invalid 'domain'"); |
| if (session->w3c_compliant && !domain.empty() && |
| !url::HostIsIPAddress(domain)) { |
| if (domain[0] == '.') |
| domain = domain.substr(1); |
| |
| if (domain.size() < 2) |
| return Status(kInvalidCookieDomain, "invalid 'domain'"); |
| |
| if (!GURL(url).DomainIs(domain)) |
| return Status(kInvalidCookieDomain, "Cookie 'domain' mismatch"); |
| |
| domain.insert(0, 1, '.'); |
| } |
| std::string path("/"); |
| if (!GetOptionalString(*cookie, "path", &path)) |
| return Status(kInvalidArgument, "invalid 'path'"); |
| std::string samesite(""); |
| if (!GetOptionalString(*cookie, "sameSite", &samesite)) |
| return Status(kInvalidArgument, "invalid 'sameSite'"); |
| if (!samesite.empty() && samesite != "Strict" && samesite != "Lax" && |
| samesite != "None") |
| return Status(kInvalidArgument, "invalid 'sameSite'"); |
| bool secure = false; |
| if (!GetOptionalBool(*cookie, "secure", &secure)) |
| return Status(kInvalidArgument, "invalid 'secure'"); |
| bool http_only = false; |
| if (!GetOptionalBool(*cookie, "httpOnly", &http_only)) |
| return Status(kInvalidArgument, "invalid 'httpOnly'"); |
| double expiry; |
| bool has_value; |
| if (session->w3c_compliant) { |
| // W3C spec says expiry is a safe integer. |
| int64_t expiry_int64; |
| if (!GetOptionalSafeInt(*cookie, "expiry", &expiry_int64, &has_value) || |
| (has_value && expiry_int64 < 0)) |
| return Status(kInvalidArgument, "invalid 'expiry'"); |
| // Use negative value to indicate expiry not specified. |
| expiry = has_value ? static_cast<double>(expiry_int64) : -1.0; |
| } else { |
| // JSON wire protocol didn't specify the type of expiry, but ChromeDriver |
| // has always accepted double, so we keep that in legacy mode. |
| if (!GetOptionalDouble(*cookie, "expiry", &expiry, &has_value) || |
| (has_value && expiry < 0)) |
| return Status(kInvalidArgument, "invalid 'expiry'"); |
| if (!has_value) |
| expiry = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() + |
| kDefaultCookieExpiryTime; |
| } |
| return web_view->AddCookie(*name, url, *cookie_value, domain, path, samesite, |
| secure, http_only, expiry); |
| } |
| |
| Status ExecuteDeleteCookie(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* name = params.FindString("name"); |
| if (!name) |
| return Status(kInvalidArgument, "missing 'name'"); |
| std::unique_ptr<base::Value> value_url; |
| std::string url; |
| Status status = GetUrl(web_view, session->GetCurrentFrameId(), &url); |
| if (status.IsError()) |
| return status; |
| |
| std::list<Cookie> cookies; |
| status = GetVisibleCookies(session, web_view, &cookies); |
| if (status.IsError()) |
| return status; |
| |
| for (std::list<Cookie>::const_iterator it = cookies.begin(); |
| it != cookies.end(); ++it) { |
| if (*name == it->name) { |
| status = web_view->DeleteCookie(it->name, url, it->domain, it->path); |
| if (status.IsError()) |
| return status; |
| } |
| } |
| return Status(kOk); |
| } |
| |
| Status ExecuteDeleteAllCookies(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| std::list<Cookie> cookies; |
| Status status = GetVisibleCookies(session, web_view, &cookies); |
| if (status.IsError()) |
| return status; |
| |
| if (!cookies.empty()) { |
| std::unique_ptr<base::Value> value_url; |
| std::string url; |
| status = GetUrl(web_view, session->GetCurrentFrameId(), &url); |
| if (status.IsError()) |
| return status; |
| for (std::list<Cookie>::const_iterator it = cookies.begin(); |
| it != cookies.end(); ++it) { |
| status = web_view->DeleteCookie(it->name, url, it->domain, it->path); |
| if (status.IsError()) |
| return status; |
| } |
| } |
| |
| return Status(kOk); |
| } |
| |
| Status ExecuteRunBounceTrackingMitigations(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| // Run command and get result |
| auto result = std::make_unique<base::Value>(base::Value::Type::DICT); |
| Status status = web_view->SendCommandAndGetResult( |
| "Storage.runBounceTrackingMitigations", base::Value::Dict(), &result); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| if (result->GetDict().empty()) { |
| // The result dictionary should only be empty if there is no bounce tracking |
| // mitigations service (DIPSService) for the current browser context. |
| return Status( |
| kUnsupportedOperation, |
| "current remote end configuration does not support bounce tracking " |
| "mitigations"); |
| } |
| |
| const base::Value::List* deleted_sites = |
| result->GetDict().FindList("deletedSites"); |
| |
| // create copies of items `deleted_sites` and add them to the output list. |
| auto site_list = std::make_unique<base::Value>(base::Value::Type::LIST); |
| for (const base::Value& site : *deleted_sites) { |
| if (!site.is_string()) { |
| return Status(kUnknownError, |
| "DevTools returns a non-string bounce tracker site"); |
| } |
| site_list->GetList().Append(site.GetString()); |
| } |
| |
| *value = std::move(site_list); |
| |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetRPHRegistrationMode(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* mode = params.FindString("mode"); |
| if (!mode) { |
| return Status(kInvalidArgument, "missing parameter 'mode'"); |
| } |
| |
| base::Value::Dict body; |
| body.Set("mode", *mode); |
| |
| return web_view->SendCommandAndGetResult("Page.setRPHRegistrationMode", body, |
| value); |
| } |
| |
| Status ExecuteSetLocation(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const base::Value::Dict* location = params.FindDict("location"); |
| Geoposition geoposition; |
| if (!location) |
| return Status(kInvalidArgument, "missing or invalid 'location'"); |
| |
| std::optional<double> maybe_latitude = location->FindDouble("latitude"); |
| if (!maybe_latitude.has_value()) |
| return Status(kInvalidArgument, "missing or invalid 'location.latitude'"); |
| geoposition.latitude = maybe_latitude.value(); |
| |
| std::optional<double> maybe_longitude = location->FindDouble("longitude"); |
| if (!maybe_longitude.has_value()) |
| return Status(kInvalidArgument, "missing or invalid 'location.longitude'"); |
| geoposition.longitude = maybe_longitude.value(); |
| |
| // |accuracy| is not part of the WebDriver spec yet, so if it is not given |
| // default to 100 meters accuracy. |
| std::optional<double> maybe_accuracy = |
| ParseDoubleIfInDictionary(*location, "accuracy", 100); |
| if (!maybe_accuracy.has_value()) |
| return Status(kInvalidArgument, "invalid 'accuracy'"); |
| geoposition.accuracy = maybe_accuracy.value(); |
| |
| Status status = web_view->OverrideGeolocation(geoposition); |
| if (status.IsOk()) { |
| session->overridden_geoposition = |
| std::make_unique<Geoposition>(geoposition); |
| } |
| return status; |
| } |
| |
| Status ExecuteSetNetworkConditions(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* network_name = params.FindString("network_name"); |
| std::unique_ptr<NetworkConditions> network_conditions( |
| new NetworkConditions()); |
| if (network_name) { |
| // Get conditions from preset list. |
| Status status = FindPresetNetwork(*network_name, network_conditions.get()); |
| if (status.IsError()) |
| return status; |
| } else if (const base::Value::Dict* conditions = |
| params.FindDict("network_conditions")) { |
| // |latency| is required. |
| std::optional<double> maybe_latency = conditions->FindDouble("latency"); |
| if (!maybe_latency.has_value()) |
| return Status(kInvalidArgument, |
| "invalid 'network_conditions' is missing 'latency'"); |
| network_conditions->latency = maybe_latency.value(); |
| |
| // Either |throughput| or the pair |download_throughput| and |
| // |upload_throughput| is required. |
| if (conditions->Find("throughput")) { |
| std::optional<double> maybe_throughput = |
| conditions->FindDouble("throughput"); |
| if (!maybe_throughput.has_value()) |
| return Status(kInvalidArgument, "invalid 'throughput'"); |
| network_conditions->upload_throughput = maybe_throughput.value(); |
| network_conditions->download_throughput = maybe_throughput.value(); |
| } else if (conditions->Find("download_throughput") && |
| conditions->Find("upload_throughput")) { |
| std::optional<double> maybe_download_throughput = |
| conditions->FindDouble("download_throughput"); |
| std::optional<double> maybe_upload_throughput = |
| conditions->FindDouble("upload_throughput"); |
| |
| if (!maybe_download_throughput.has_value() || |
| !maybe_upload_throughput.has_value()) |
| return Status(kInvalidArgument, |
| "invalid 'download_throughput' or 'upload_throughput'"); |
| network_conditions->download_throughput = |
| maybe_download_throughput.value(); |
| network_conditions->upload_throughput = maybe_upload_throughput.value(); |
| } else { |
| return Status(kInvalidArgument, |
| "invalid 'network_conditions' is missing 'throughput' or " |
| "'download_throughput'/'upload_throughput' pair"); |
| } |
| |
| // |offline| is optional. |
| if (const base::Value* offline = conditions->Find("offline")) { |
| if (!offline->is_bool()) |
| return Status(kInvalidArgument, "invalid 'offline'"); |
| network_conditions->offline = offline->GetBool(); |
| } else { |
| network_conditions->offline = false; |
| } |
| } else { |
| return Status(kInvalidArgument, |
| "either 'network_conditions' or 'network_name' must be " |
| "supplied"); |
| } |
| |
| session->overridden_network_conditions.reset( |
| network_conditions.release()); |
| return web_view->OverrideNetworkConditions( |
| *session->overridden_network_conditions); |
| } |
| |
| Status ExecuteDeleteNetworkConditions(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| // Chrome does not have any command to stop overriding network conditions, so |
| // we just override the network conditions with the "No throttling" preset. |
| NetworkConditions network_conditions; |
| // Get conditions from preset list. |
| Status status = FindPresetNetwork("No throttling", &network_conditions); |
| if (status.IsError()) |
| return status; |
| |
| status = web_view->OverrideNetworkConditions(network_conditions); |
| if (status.IsError()) |
| return status; |
| |
| // After we've successfully overridden the network conditions with |
| // "No throttling", we can delete them from |session|. |
| session->overridden_network_conditions.reset(); |
| return status; |
| } |
| |
| Status ExecuteTakeHeapSnapshot(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->TakeHeapSnapshot(value); |
| } |
| |
| Status ExecuteGetWindowRect(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Chrome::WindowRect window_rect; |
| Status status = session->chrome->GetWindowRect(session->window, &window_rect); |
| if (status.IsError()) |
| return status; |
| |
| base::Value::Dict rect; |
| rect.Set("x", window_rect.x); |
| rect.Set("y", window_rect.y); |
| rect.Set("width", window_rect.width); |
| rect.Set("height", window_rect.height); |
| value->reset(new base::Value(std::move(rect))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetWindowRect(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const double max_range = 2147483647; // 2^31 - 1 |
| const double min_range = -2147483648; // -2^31 |
| const base::Value* temp; |
| double width = 0; |
| double height = 0; |
| double x = 0; |
| double y = 0; |
| |
| temp = params.Find("x"); |
| bool has_x = temp && !temp->is_none(); |
| if (has_x) { |
| if (!temp->is_double() && !temp->is_int()) |
| return Status(kInvalidArgument, "'x' must be a number"); |
| x = temp->GetDouble(); |
| if (x > max_range || x < min_range) |
| return Status(kInvalidArgument, "'x' out of range"); |
| } |
| |
| temp = params.Find("y"); |
| bool has_y = temp && !temp->is_none(); |
| if (has_y) { |
| if (!temp->is_double() && !temp->is_int()) |
| return Status(kInvalidArgument, "'y' must be a number"); |
| y = temp->GetDouble(); |
| if (y > max_range || y < min_range) |
| return Status(kInvalidArgument, "'y' out of range"); |
| } |
| |
| temp = params.Find("width"); |
| bool has_width = temp && !temp->is_none(); |
| if (has_width) { |
| if (!temp->is_double() && !temp->is_int()) |
| return Status(kInvalidArgument, "'width' must be a number"); |
| width = temp->GetDouble(); |
| if (width > max_range || width < 0) |
| return Status(kInvalidArgument, "'width' out of range"); |
| } |
| |
| temp = params.Find("height"); |
| bool has_height = temp && !temp->is_none(); |
| if (has_height) { |
| if (!temp->is_double() && !temp->is_int()) |
| return Status(kInvalidArgument, "'height' must be a number"); |
| height = temp->GetDouble(); |
| if (height > max_range || height < 0) |
| return Status(kInvalidArgument, "'height' out of range"); |
| } |
| |
| // to pass to the set window rect command |
| base::Value::Dict rect_params; |
| // only set position if both x and y are given |
| if (has_x && has_y) { |
| rect_params.Set("x", static_cast<int>(x)); |
| rect_params.Set("y", static_cast<int>(y)); |
| } // only set size if both height and width are given |
| if (has_width && has_height) { |
| rect_params.Set("width", static_cast<int>(width)); |
| rect_params.Set("height", static_cast<int>(height)); |
| } |
| Status status = session->chrome->SetWindowRect(session->window, rect_params); |
| if (status.IsError()) |
| return status; |
| |
| // return the current window rect |
| return ExecuteGetWindowRect(session, web_view, params, value, timeout); |
| } |
| |
| Status ExecuteMaximizeWindow(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Status status = session->chrome->MaximizeWindow(session->window); |
| if (status.IsError()) |
| return status; |
| |
| return ExecuteGetWindowRect(session, web_view, params, value, timeout); |
| } |
| |
| Status ExecuteMinimizeWindow(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Status status = session->chrome->MinimizeWindow(session->window); |
| if (status.IsError()) |
| return status; |
| |
| return ExecuteGetWindowRect(session, web_view, params, value, timeout); |
| } |
| |
| Status ExecuteFullScreenWindow(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| Status status = session->chrome->FullScreenWindow(session->window); |
| if (status.IsError()) |
| return status; |
| |
| return ExecuteGetWindowRect(session, web_view, params, value, timeout); |
| } |
| |
| Status ExecuteSetSinkToUse(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Cast.setSinkToUse", params); |
| } |
| |
| Status ExecuteStartDesktopMirroring(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Cast.startDesktopMirroring", params); |
| } |
| |
| Status ExecuteStartTabMirroring(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Cast.startTabMirroring", params); |
| } |
| |
| Status ExecuteStopCasting(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Cast.stopCasting", params); |
| } |
| |
| Status ExecuteGetSinks(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| *value = web_view->GetCastSinks(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetIssueMessage(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| *value = web_view->GetCastIssueMessage(); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetPermission(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const base::Value::Dict* descriptor = params.FindDict("descriptor"); |
| if (!descriptor) |
| return Status(kInvalidArgument, "no descriptor dictionary"); |
| |
| const std::string* name = descriptor->FindString("name"); |
| if (!name) |
| return Status(kInvalidArgument, "no name in descriptor"); |
| |
| const std::string* permission_state = params.FindString("state"); |
| if (!permission_state) |
| return Status(kInvalidArgument, "no permission state"); |
| |
| Chrome::PermissionState valid_state; |
| if (*permission_state == "granted") |
| valid_state = Chrome::PermissionState::kGranted; |
| else if (*permission_state == "denied") |
| valid_state = Chrome::PermissionState::kDenied; |
| else if (*permission_state == "prompt") |
| valid_state = Chrome::PermissionState::kPrompt; |
| else |
| return Status(kInvalidArgument, "unrecognized permission state"); |
| |
| auto dict = std::make_unique<base::Value::Dict>(descriptor->Clone()); |
| return session->chrome->SetPermission(std::move(dict), valid_state, web_view, |
| session->GetCurrentFrameId()); |
| } |
| |
| Status ExecuteSetDevicePosture(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| const std::string* posture = params.FindString("posture"); |
| if (!posture) { |
| return Status(kInvalidArgument, "'posture' must be a string"); |
| } |
| base::Value::Dict args; |
| args.Set("posture", base::Value::Dict().Set("type", *posture)); |
| return web_view->SendCommand("Emulation.setDevicePostureOverride", args); |
| } |
| |
| Status ExecuteClearDevicePosture(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Emulation.clearDevicePostureOverride", |
| base::Value::Dict()); |
| } |
| |
| Status ExecuteSetDisplayFeatures(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| bool has_value; |
| const base::Value::List* features_list = nullptr; |
| if (!GetOptionalList(params, "features", &features_list, &has_value)) { |
| return Status(kInvalidArgument, "'features' must be an array"); |
| } |
| |
| if (!has_value) { |
| return Status(kInvalidArgument, "'features' must have a value"); |
| } |
| |
| for (const base::Value& feature : *features_list) { |
| if (!feature.is_dict()) { |
| return Status(kInvalidArgument, "a feature must be a dictionary"); |
| } |
| const auto& feature_dict = feature.GetDict(); |
| std::optional<int> mask = feature_dict.FindInt("maskLength"); |
| if (!mask) { |
| return Status(kInvalidArgument, |
| "a feature must contain the maskLength attribute"); |
| } else if (mask.value() < 0) { |
| return Status(kInvalidArgument, |
| "a feature must have a positive maskLength attribute"); |
| } |
| |
| std::optional<int> offset = feature_dict.FindInt("offset"); |
| if (!offset) { |
| return Status(kInvalidArgument, |
| "a feature must contain the offset attribute"); |
| } else if (offset.value() < 0) { |
| return Status(kInvalidArgument, |
| "a feature must have a positive offset attribute"); |
| } |
| |
| const std::string* orientation = feature_dict.FindString("orientation"); |
| if (!orientation) { |
| return Status(kInvalidArgument, |
| "a feature must contain the orientation attribute"); |
| } |
| } |
| return web_view->SendCommand("Emulation.setDisplayFeaturesOverride", params); |
| } |
| |
| Status ExecuteClearDisplayFeatures(Session* session, |
| WebView* web_view, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value, |
| Timeout* timeout) { |
| return web_view->SendCommand("Emulation.clearDisplayFeaturesOverride", |
| base::Value::Dict()); |
| } |