Path: blob/trunk/rb/lib/selenium/webdriver/remote/bridge.rb
3992 views
# frozen_string_literal: true12# Licensed to the Software Freedom Conservancy (SFC) under one3# or more contributor license agreements. See the NOTICE file4# distributed with this work for additional information5# regarding copyright ownership. The SFC licenses this file6# to you under the Apache License, Version 2.0 (the7# "License"); you may not use this file except in compliance8# with the License. You may obtain a copy of the License at9#10# http://www.apache.org/licenses/LICENSE-2.011#12# Unless required by applicable law or agreed to in writing,13# software distributed under the License is distributed on an14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY15# KIND, either express or implied. See the License for the16# specific language governing permissions and limitations17# under the License.1819module Selenium20module WebDriver21module Remote22class Bridge23autoload :COMMANDS, 'selenium/webdriver/remote/bridge/commands'24autoload :LocatorConverter, 'selenium/webdriver/remote/bridge/locator_converter'2526include Atoms2728PORT = 44442930attr_accessor :http, :file_detector31attr_reader :capabilities3233class << self34attr_reader :extra_commands35attr_writer :element_class, :locator_converter3637def add_command(name, verb, url, &block)38@extra_commands ||= {}39@extra_commands[name] = [verb, url]40define_method(name, &block)41end4243def locator_converter44@locator_converter ||= LocatorConverter.new45end4647def element_class48@element_class ||= Element49end50end5152#53# Initializes the bridge with the given server URL54# @param [String, URI] url url for the remote server55# @param [Object] http_client an HTTP client instance that implements the same protocol as Http::Default56# @api private57#5859def initialize(url:, http_client: nil)60uri = url.is_a?(URI) ? url : URI.parse(url)61uri.path += '/' unless uri.path.end_with?('/')6263@http = http_client || Http::Default.new64@http.server_url = uri65@file_detector = nil6667@locator_converter = self.class.locator_converter68end6970#71# Creates session.72#7374def create_session(capabilities)75response = execute(:new_session, {}, prepare_capabilities_payload(capabilities))7677@session_id = response['sessionId']78capabilities = response['capabilities']7980raise Error::WebDriverError, 'no sessionId in returned payload' unless @session_id8182@capabilities = Capabilities.json_create(capabilities)8384case @capabilities[:browser_name]85when 'chrome', 'chrome-headless-shell'86extend(WebDriver::Chrome::Features)87when 'firefox'88extend(WebDriver::Firefox::Features)89when 'msedge', 'MicrosoftEdge'90extend(WebDriver::Edge::Features)91when 'Safari', 'Safari Technology Preview'92extend(WebDriver::Safari::Features)93when 'internet explorer'94extend(WebDriver::IE::Features)95end96end9798#99# Returns the current session ID.100#101102def session_id103@session_id || raise(Error::WebDriverError, 'no current session exists')104end105106def browser107@browser ||= begin108name = @capabilities.browser_name109name ? name.tr(' -', '_').downcase.to_sym : 'unknown'110end111end112113def status114execute :status115end116117def get(url)118execute :get, {}, {url: url}119end120121#122# timeouts123#124125def timeouts126execute :get_timeouts, {}127end128129def timeouts=(timeouts)130execute :set_timeout, {}, timeouts131end132133#134# alerts135#136137def accept_alert138execute :accept_alert139end140141def dismiss_alert142execute :dismiss_alert143end144145def alert=(keys)146execute :send_alert_text, {}, {value: keys.chars, text: keys}147end148149def alert_text150execute :get_alert_text151end152153#154# navigation155#156157def go_back158execute :back159end160161def go_forward162execute :forward163end164165def url166execute :get_current_url167end168169def title170execute :get_title171end172173def page_source174execute :get_page_source175end176177#178# Create a new top-level browsing context179# https://w3c.github.io/webdriver/#new-window180# @param type [String] Supports two values: 'tab' and 'window'.181# Use 'tab' if you'd like the new window to share an OS-level window182# with the current browsing context.183# Use 'window' otherwise184# @return [Hash] Containing 'handle' with the value of the window handle185# and 'type' with the value of the created window type186#187def new_window(type)188execute :new_window, {}, {type: type}189end190191def switch_to_window(name)192execute :switch_to_window, {}, {handle: name}193end194195def switch_to_frame(id)196id = find_element_by('id', id) if id.is_a? String197execute :switch_to_frame, {}, {id: id}198end199200def switch_to_parent_frame201execute :switch_to_parent_frame202end203204def switch_to_default_content205switch_to_frame nil206end207208QUIT_ERRORS = [IOError, EOFError, WebSocket::Error].freeze209210def quit211begin212execute :delete_session213rescue *QUIT_ERRORS => e214WebDriver.logger.debug "delete_session failed during quit: #{e.class}: #{e.message}", id: :quit215ensure216begin217http.close218rescue *QUIT_ERRORS => e219WebDriver.logger.debug "http.close failed during quit: #{e.class}: #{e.message}", id: :quit220end221end222nil223end224225def close226execute :close_window227end228229def refresh230execute :refresh231end232233#234# window handling235#236237def window_handles238execute :get_window_handles239end240241def window_handle242execute :get_window_handle243end244245def resize_window(width, height, handle = :current)246raise Error::WebDriverError, 'Switch to desired window before changing its size' unless handle == :current247248set_window_rect(width: width, height: height)249end250251def window_size(handle = :current)252unless handle == :current253raise Error::UnsupportedOperationError,254'Switch to desired window before getting its size'255end256257data = execute :get_window_rect258Dimension.new data['width'], data['height']259end260261def minimize_window262execute :minimize_window263end264265def maximize_window(handle = :current)266unless handle == :current267raise Error::UnsupportedOperationError,268'Switch to desired window before changing its size'269end270271execute :maximize_window272end273274def full_screen_window275execute :fullscreen_window276end277278def reposition_window(x, y)279set_window_rect(x: x, y: y)280end281282def window_position283data = execute :get_window_rect284Point.new data['x'], data['y']285end286287def set_window_rect(x: nil, y: nil, width: nil, height: nil)288params = {x: x, y: y, width: width, height: height}289params.update(params) { |_k, v| Integer(v) unless v.nil? }290execute :set_window_rect, {}, params291end292293def window_rect294data = execute :get_window_rect295Rectangle.new data['x'], data['y'], data['width'], data['height']296end297298def screenshot299execute :take_screenshot300end301302def element_screenshot(element)303execute :take_element_screenshot, id: element304end305306#307# javascript execution308#309310def execute_script(script, *args)311result = execute :execute_script, {}, {script: script, args: args}312unwrap_script_result result313end314315def execute_async_script(script, *args)316result = execute :execute_async_script, {}, {script: script, args: args}317unwrap_script_result result318end319320#321# cookies322#323324def manage325@manage ||= WebDriver::Manager.new(self)326end327328def add_cookie(cookie)329execute :add_cookie, {}, {cookie: cookie}330end331332def delete_cookie(name)333raise ArgumentError, 'Cookie name cannot be null or empty' if name.nil? || name.to_s.strip.empty?334335execute :delete_cookie, name: name336end337338def cookie(name)339execute :get_cookie, name: name340end341342def cookies343execute :get_all_cookies344end345346def delete_all_cookies347execute :delete_all_cookies348end349350#351# actions352#353354def action(async: false, devices: [], duration: 250)355ActionBuilder.new self, async: async, devices: devices, duration: duration356end357alias actions action358359def send_actions(data)360execute :actions, {}, {actions: data}361end362363def release_actions364execute :release_actions365end366367def print_page(options = {})368execute :print_page, {}, {options: options}369end370371def click_element(element)372execute :element_click, id: element373end374375def send_keys_to_element(element, keys)376keys = upload_if_necessary(keys) if @file_detector377text = keys.join378execute :element_send_keys, {id: element}, {value: text.chars, text: text}379end380381def clear_element(element)382execute :element_clear, id: element383end384385def submit_element(element)386script = "/* submitForm */ var form = arguments[0];\n" \387"while (form.nodeName != \"FORM\" && form.parentNode) {\n " \388"form = form.parentNode;\n" \389"}\n" \390"if (!form) { throw Error('Unable to find containing form element'); }\n" \391"if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n" \392"var e = form.ownerDocument.createEvent('Event');\n" \393"e.initEvent('submit', true, true);\n" \394"if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"395396execute_script(script, Bridge.element_class::ELEMENT_KEY => element)397rescue Error::JavascriptError398raise Error::UnsupportedOperationError, 'To submit an element, it must be nested inside a form element'399end400401#402# element properties403#404405def element_tag_name(element)406execute :get_element_tag_name, id: element407end408409def element_attribute(element, name)410WebDriver.logger.debug "Using script for :getAttribute of #{name}", id: :script411execute_atom :getAttribute, element, name412end413414def element_dom_attribute(element, name)415execute :get_element_attribute, id: element, name: name416end417418def element_property(element, name)419execute :get_element_property, id: element, name: name420end421422def element_aria_role(element)423execute :get_element_aria_role, id: element424end425426def element_aria_label(element)427execute :get_element_aria_label, id: element428end429430def element_value(element)431element_property element, 'value'432end433434def element_text(element)435execute :get_element_text, id: element436end437438def element_location(element)439data = execute :get_element_rect, id: element440441Point.new data['x'], data['y']442end443444def element_rect(element)445data = execute :get_element_rect, id: element446447Rectangle.new data['x'], data['y'], data['width'], data['height']448end449450def element_location_once_scrolled_into_view(element)451send_keys_to_element(element, [''])452element_location(element)453end454455def element_size(element)456data = execute :get_element_rect, id: element457458Dimension.new data['width'], data['height']459end460461def element_enabled?(element)462execute :is_element_enabled, id: element463end464465def element_selected?(element)466execute :is_element_selected, id: element467end468469def element_displayed?(element)470WebDriver.logger.debug 'Using script for :isDisplayed', id: :script471execute_atom :isDisplayed, element472end473474def element_value_of_css_property(element, prop)475execute :get_element_css_value, id: element, property_name: prop476end477478#479# finding elements480#481482def active_element483Bridge.element_class.new self, element_id_from(execute(:get_active_element))484end485486alias switch_to_active_element active_element487488def find_element_by(how, what, parent_ref = [])489how, what = @locator_converter.convert(how, what)490491return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'492493parent_type, parent_id = parent_ref494id = case parent_type495when :element496execute :find_child_element, {id: parent_id}, {using: how, value: what.to_s}497when :shadow_root498execute :find_shadow_child_element, {id: parent_id}, {using: how, value: what.to_s}499else500execute :find_element, {}, {using: how, value: what.to_s}501end502503Bridge.element_class.new self, element_id_from(id)504end505506def find_elements_by(how, what, parent_ref = [])507how, what = @locator_converter.convert(how, what)508509return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'510511parent_type, parent_id = parent_ref512ids = case parent_type513when :element514execute :find_child_elements, {id: parent_id}, {using: how, value: what.to_s}515when :shadow_root516execute :find_shadow_child_elements, {id: parent_id}, {using: how, value: what.to_s}517else518execute :find_elements, {}, {using: how, value: what.to_s}519end520521ids.map { |id| Bridge.element_class.new self, element_id_from(id) }522end523524def shadow_root(element)525id = execute :get_element_shadow_root, id: element526ShadowRoot.new self, shadow_root_id_from(id)527end528529#530# virtual-authenticator531#532533def add_virtual_authenticator(options)534authenticator_id = execute :add_virtual_authenticator, {}, options.as_json535VirtualAuthenticator.new(self, authenticator_id, options)536end537538def remove_virtual_authenticator(id)539execute :remove_virtual_authenticator, {authenticatorId: id}540end541542def add_credential(credential, id)543execute :add_credential, {authenticatorId: id}, credential544end545546def credentials(authenticator_id)547execute :get_credentials, {authenticatorId: authenticator_id}548end549550def remove_credential(credential_id, authenticator_id)551execute :remove_credential, {credentialId: credential_id, authenticatorId: authenticator_id}552end553554def remove_all_credentials(authenticator_id)555execute :remove_all_credentials, {authenticatorId: authenticator_id}556end557558def user_verified(verified, authenticator_id)559execute :set_user_verified, {authenticatorId: authenticator_id}, {isUserVerified: verified}560end561562#563# federated-credential management564#565566def cancel_fedcm_dialog567execute :cancel_fedcm_dialog568end569570def select_fedcm_account(index)571execute :select_fedcm_account, {}, {accountIndex: index}572end573574def fedcm_dialog_type575execute :get_fedcm_dialog_type576end577578def fedcm_title579execute(:get_fedcm_title).fetch('title')580end581582def fedcm_subtitle583execute(:get_fedcm_title).fetch('subtitle', nil)584end585586def fedcm_account_list587execute :get_fedcm_account_list588end589590def fedcm_delay(enabled)591execute :set_fedcm_delay, {}, {enabled: enabled}592end593594def reset_fedcm_cooldown595execute :reset_fedcm_cooldown596end597598def click_fedcm_dialog_button599execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}600end601602def bidi603msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'604raise(WebDriver::Error::WebDriverError, msg)605end606607def command_list608COMMANDS609end610611private612613#614# executes a command on the remote server.615#616# @return [WebDriver::Remote::Response]617#618619def execute(command, opts = {}, command_hash = nil)620verb, path = commands(command) || raise(ArgumentError, "unknown command: #{command.inspect}")621path = path.dup622623path[':session_id'] = session_id if path.include?(':session_id')624625begin626opts.each { |key, value| path[key.inspect] = escaper.escape(value.to_s) }627rescue IndexError628raise ArgumentError, "#{opts.inspect} invalid for #{command.inspect}"629end630631WebDriver.logger.debug("-> #{verb.to_s.upcase} #{path}", id: :command)632http.call(verb, path, command_hash)['value']633end634635def escaper636@escaper ||= defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER637end638639def commands(command)640command_list[command] || Bridge.extra_commands[command]641end642643def unwrap_script_result(arg)644case arg645when Array646arg.map { |e| unwrap_script_result(e) }647when Hash648element_id = element_id_from(arg)649return Bridge.element_class.new(self, element_id) if element_id650651shadow_root_id = shadow_root_id_from(arg)652return ShadowRoot.new self, shadow_root_id if shadow_root_id653654arg.each { |k, v| arg[k] = unwrap_script_result(v) }655else656arg657end658end659660def element_id_from(id)661id['ELEMENT'] || id[Bridge.element_class::ELEMENT_KEY]662end663664def shadow_root_id_from(id)665id[ShadowRoot::ROOT_KEY]666end667668def prepare_capabilities_payload(capabilities)669capabilities = {alwaysMatch: capabilities} if !capabilities['alwaysMatch'] && !capabilities['firstMatch']670{capabilities: capabilities}671end672end # Bridge673end # Remote674end # WebDriver675end # Selenium676677678