diff --git a/rb/lib/selenium/webdriver/bidi/browsing_context.rb b/rb/lib/selenium/webdriver/bidi/browsing_context.rb index 8e4eaa1434e0b..b8f33227ce8a6 100644 --- a/rb/lib/selenium/webdriver/bidi/browsing_context.rb +++ b/rb/lib/selenium/webdriver/bidi/browsing_context.rb @@ -17,72 +17,84 @@ # specific language governing permissions and limitations # under the License. -require_relative 'navigate_result' -require_relative 'browsing_context_info' - module Selenium module WebDriver class BiDi + # Implements the browsingContext Module of the WebDriver-BiDi specification + # + # @api private + # class BrowsingContext - attr_accessor :id - READINESS_STATE = { - none: 'none', - interactive: 'interactive', - complete: 'complete' + 'none' => 'none', + 'eager' => 'interactive', + 'normal' => 'complete' }.freeze - def initialize(driver:, browsing_context_id: nil, type: nil, reference_context: nil) - unless driver.capabilities.web_socket_url - raise Error::WebDriverError, - 'WebDriver instance must support BiDi protocol' - end - - unless type.nil? || %i[window tab].include?(type) - raise ArgumentError, - "Valid types are :window & :tab. Received: #{type.inspect}" - end - - @bidi = driver.bidi - @id = browsing_context_id.nil? ? create(type, reference_context)['context'] : browsing_context_id + # TODO: store current window handle in bridge object instead of always calling it + def initialize(bridge) + @bridge = bridge + @bidi = @bridge.bidi + page_load_strategy = bridge.capabilities[:page_load_strategy] + @readiness = READINESS_STATE[page_load_strategy] end - def navigate(url:, readiness_state: nil) - unless readiness_state.nil? || READINESS_STATE.key?(readiness_state) - raise ArgumentError, - "Valid readiness states are :none, :interactive & :complete. Received: #{readiness_state.inspect}" - end - - navigate_result = @bidi.send_cmd('browsingContext.navigate', context: @id, url: url, - wait: READINESS_STATE[readiness_state]) - - NavigateResult.new( - url: navigate_result['url'], - navigation_id: navigate_result['navigation'] - ) + # Navigates to the specified URL in the given browsing context. + # + # @param url [String] The URL to navigate to. + # @param context_id [String, NilClass] The ID of the browsing context to navigate in. + # Defaults to the window handle of the current context. + def navigate(url, context_id: nil) + context_id ||= @bridge.window_handle + @bidi.send_cmd('browsingContext.navigate', context: context_id, url: url, wait: @readiness) end - def get_tree(max_depth: nil) - result = @bidi.send_cmd('browsingContext.getTree', root: @id, maxDepth: max_depth).dig('contexts', 0) - - BrowsingContextInfo.new( - id: result['context'], - url: result['url'], - children: result['children'], - parent_context: result['parent'] - ) + # Traverses the browsing context history by a given delta. + # + # @param delta [Integer] The number of steps to traverse. + # Positive values go forwards, negative values go backwards. + # @param context_id [String, NilClass] The ID of the context to traverse. + # Defaults to the window handle of the current context. + def traverse_history(delta, context_id: nil) + context_id ||= @bridge.window_handle + @bidi.send_cmd('browsingContext.traverseHistory', context: context_id, delta: delta) end - def close - @bidi.send_cmd('browsingContext.close', context: @id) + # Reloads the browsing context. + # @param [String, NilClass] context_id The ID of the context to reload. + # Defaults to the window handle of the current context. + # @param [Boolean] ignore_cache Whether to bypass the cache when reloading. + # Defaults to false. + def reload(context_id: nil, ignore_cache: false) + context_id ||= @bridge.window_handle + params = {context: context_id, ignore_cache: ignore_cache, wait: @readiness} + @bidi.send_cmd('browsingContext.reload', **params) end - private + # Closes the browsing context. + # + # @param [String] context_id The ID of the context to close. + # Defaults to the window handle of the current context. + def close(context_id: nil) + context_id ||= @bridge.window_handle + @bidi.send_cmd('browsingContext.close', context: context_id) + end - def create(type, reference_context) - @bidi.send_cmd('browsingContext.create', type: type.to_s, referenceContext: reference_context) + # Create a new browsing context. + # + # @param [Symbol] type The type of browsing context to create. + # Valid options are :tab and :window with :window being the default + # @param [String] context_id The reference context for the new browsing context. + # Defaults to the current window handle. + # + # @return [String] The context ID of the created browsing context. + def create(type: nil, context_id: nil) + type ||= :window + context_id ||= @bridge.window_handle + result = @bidi.send_cmd('browsingContext.create', type: type.to_s, referenceContext: context_id) + result['context'] end - end # BrowsingContext + end end # BiDi end # WebDriver end # Selenium diff --git a/rb/lib/selenium/webdriver/bidi/browsing_context_info.rb b/rb/lib/selenium/webdriver/bidi/browsing_context_info.rb deleted file mode 100644 index 8a4435c0cd67a..0000000000000 --- a/rb/lib/selenium/webdriver/bidi/browsing_context_info.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -module Selenium - module WebDriver - class BiDi - class BrowsingContextInfo - attr_accessor :id, :url, :children, :parent_browsing_context - - def initialize(id:, url:, children:, parent_context:) - @id = id - @url = url - @children = children - @parent_browsing_context = parent_context - end - end # BrowsingContextInfo - end # BiDi - end # WebDriver -end # Selenium diff --git a/rb/lib/selenium/webdriver/bidi/navigate_result.rb b/rb/lib/selenium/webdriver/bidi/navigate_result.rb deleted file mode 100644 index ad32c28ea952b..0000000000000 --- a/rb/lib/selenium/webdriver/bidi/navigate_result.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -module Selenium - module WebDriver - class BiDi - class NavigateResult - attr_accessor :url, :navigation_id - - def initialize(url:, navigation_id:) - @url = url - @navigation_id = navigation_id - end - end # NavigateResult - end # BiDi - end # WebDriver -end # Selenium diff --git a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb index 935fdff5b2954..c95ddec538f85 100644 --- a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb @@ -29,6 +29,22 @@ def create_session(capabilities) @bidi = Selenium::WebDriver::BiDi.new(url: socket_url) end + def get(url) + browsing_context.navigate(url) + end + + def go_back + browsing_context.traverse_history(-1) + end + + def go_forward + browsing_context.traverse_history(1) + end + + def refresh + browsing_context.reload + end + def quit super ensure @@ -38,6 +54,12 @@ def quit def close execute(:close_window).tap { |handles| bidi.close if handles.empty? } end + + private + + def browsing_context + @browsing_context ||= WebDriver::BiDi::BrowsingContext.new(self) + end end # BiDiBridge end # Remote end # WebDriver diff --git a/rb/sig/lib/selenium/webdriver/bidi/browsing_context.rbs b/rb/sig/lib/selenium/webdriver/bidi/browsing_context.rbs index a690a38221634..1577c22073561 100644 --- a/rb/sig/lib/selenium/webdriver/bidi/browsing_context.rbs +++ b/rb/sig/lib/selenium/webdriver/bidi/browsing_context.rbs @@ -2,25 +2,21 @@ module Selenium module WebDriver class BiDi class BrowsingContext - @bidi: untyped + @bidi: BiDi - @id: untyped + READINESS_STATE: Hash[String, String] - attr_accessor id: untyped + def initialize: (Remote::Bridge bridge) -> void - READINESS_STATE: Hash[Symbol, String] + def navigate: (String url, String? context_id) -> void - def initialize: (driver: untyped, ?browsing_context_id: untyped?, ?type: untyped?, ?reference_context: untyped?) -> void + def traverse_history: (Integer delta, String? context_id) -> void - def navigate: (url: untyped, ?readiness_state: untyped?) -> untyped + def reload: (String? context_id, ?ignore_cache: bool) -> void - def get_tree: (?max_depth: untyped?) -> untyped + def close: (String? context_id) -> void - def close: () -> untyped - - private - - def create: (untyped type, untyped reference_context) -> untyped + def create: (?type: Symbol | String | nil, ?context_id: String | nil) -> String end end end diff --git a/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs b/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs index bb0c77cc36863..aeec5854d18cf 100644 --- a/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs +++ b/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs @@ -1,16 +1,28 @@ module Selenium module WebDriver module Remote - class BiDiBridge < Bridge - @bidi: untyped + class BiDiBridge + @browsing_context: BiDi::BrowsingContext - attr_reader bidi: untyped + attr_reader bidi: BiDi - def create_session: (Hash[Symbol, String] capabilities) -> BiDi + def create_session: (untyped capabilities) -> void - def quit: () -> nil + def get: (String url) -> void - def close: () -> untyped + def go_back: () -> void + + def go_forward: () -> void + + def refresh: () -> void + + def quit: () -> void + + def close: () -> void + + private + + def browsing_context: () -> BiDi::BrowsingContext end end end diff --git a/rb/spec/integration/selenium/webdriver/bidi/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/browsing_context_spec.rb index 902a289441652..353847a4c0bd7 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/browsing_context_spec.rb @@ -26,85 +26,54 @@ class BiDi only: {browser: %i[chrome edge firefox]} do after { |example| reset_driver!(example: example) } - it 'can create a browsing context for given id' do - id = driver.window_handle - browsing_context = described_class.new(driver: driver, browsing_context_id: id) - expect(browsing_context.id).to eq(id) - end + let(:bridge) { driver.instance_variable_get(:@bridge) } - it 'can create a window' do - browsing_context = described_class.new(driver: driver, type: :window) - expect(browsing_context.id).not_to be_nil - end + describe '#create' do + it 'without arguments' do + id = described_class.new(bridge).create - it 'can create a window with a reference context' do - browsing_context = described_class.new(driver: driver, type: :window, - reference_context: driver.window_handle) - expect(browsing_context.id).not_to be_nil - end + expect(driver.window_handles).to include(id) + end - it 'can create a tab without a reference context' do - browsing_context = described_class.new(driver: driver, type: :tab) - expect(browsing_context.id).not_to be_nil - end + it 'accepts a tab type' do + id = described_class.new(bridge).create(type: :tab) - it 'can create a tab with a reference context' do - browsing_context = described_class.new(driver: driver, type: :tab, reference_context: driver.window_handle) - expect(browsing_context.id).not_to be_nil - end - - it 'can navigate to a url' do - browsing_context = described_class.new(driver: driver, type: :tab) + expect(driver.window_handles).to include(id) + end - info = browsing_context.navigate url: url_for('/bidi/logEntryAdded.html') + it 'accepts a window type' do + id = described_class.new(bridge).create(type: :window) - expect(browsing_context.id).not_to be_nil - expect(info.url).to include('/bidi/logEntryAdded.html') - end + expect(driver.window_handles).to include(id) + end - it 'can navigate to a url with readiness state' do - browsing_context = described_class.new(driver: driver, type: :tab) - - info = browsing_context.navigate url: url_for('/bidi/logEntryAdded.html'), - readiness_state: :complete - - expect(browsing_context.id).not_to be_nil - expect(info.url).to include('/bidi/logEntryAdded.html') - end - - it 'can get tree with a child' do - browsing_context_id = driver.window_handle - parent_window = described_class.new(driver: driver, browsing_context_id: browsing_context_id) - parent_window.navigate(url: url_for('iframes.html'), - readiness_state: :complete) - - context_info = parent_window.get_tree - expect(context_info.children.size).to eq(1) - expect(context_info.id).to eq(browsing_context_id) - expect(context_info.children[0]['url']).to include('formPage.html') - end + it 'errors on unknown type' do + msg = /invalid argument: Invalid enum value. Expected 'tab' | 'window', received 'unknown'/ + expect { + described_class.new(bridge).create(type: :unknown) + }.to raise_error(Error::WebDriverError, msg) + end - it 'can get tree with depth' do - browsing_context_id = driver.window_handle - parent_window = described_class.new(driver: driver, browsing_context_id: browsing_context_id) - parent_window.navigate(url: url_for('iframes.html'), - readiness_state: :complete) + it 'accepts a reference context' do + id = driver.window_handle + result = described_class.new(bridge).create(context_id: id) - context_info = parent_window.get_tree(max_depth: 0) - expect(context_info.children).to be_nil - expect(context_info.id).to eq(browsing_context_id) + expect(driver.window_handles).to include(id, result) + end end - it 'can close a window' do - window1 = described_class.new(driver: driver, type: :window) - window2 = described_class.new(driver: driver, type: :window) + it 'closes a window' do + browsing_context = described_class.new(bridge) + window1 = browsing_context.create + window2 = browsing_context.create - window2.close + browsing_context.close(context_id: window2) - expect { window1.get_tree }.not_to raise_error - expect { window2.get_tree }.to raise_error(Error::WebDriverError) + handles = driver.window_handles + expect(handles).to include(window1) + expect(handles).not_to include(window2) end - end # BrowsingContext + end end # BiDi end # WebDriver end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/navigation_spec.rb b/rb/spec/integration/selenium/webdriver/navigation_spec.rb index b7586bc1bb9d8..1f7f5506c4b4b 100644 --- a/rb/spec/integration/selenium/webdriver/navigation_spec.rb +++ b/rb/spec/integration/selenium/webdriver/navigation_spec.rb @@ -18,43 +18,46 @@ # under the License. require_relative 'spec_helper' +module Selenium + module WebDriver + describe Navigation do + it 'navigates back and forward' do + form_title = 'We Leave From Here' + result_title = 'We Arrive Here' + form_url = url_for 'formPage.html' + result_url = url_for 'resultPage.html' -describe 'Navigation', exclusive: {bidi: false, reason: 'Not yet implemented with BiDi'} do - it 'navigates back and forward' do - form_title = 'We Leave From Here' - result_title = 'We Arrive Here' - form_url = url_for 'formPage.html' - result_url = url_for 'resultPage.html' + driver.navigate.to form_url + expect(driver.title).to eq(form_title) - driver.navigate.to form_url - expect(driver.title).to eq(form_title) + driver.find_element(id: 'imageButton').click + wait.until { driver.title != form_title } - driver.find_element(id: 'imageButton').click - wait.until { driver.title != form_title } + expect(driver.current_url).to include(result_url) + expect(driver.title).to eq(result_title) - expect(driver.current_url).to include(result_url) - expect(driver.title).to eq(result_title) + driver.navigate.back - driver.navigate.back + expect(driver.current_url).to include(form_url) + expect(driver.title).to eq(form_title) - expect(driver.current_url).to include(form_url) - expect(driver.title).to eq(form_title) + driver.navigate.forward + expect(driver.current_url).to include(result_url) + expect(driver.title).to eq(result_title) + end - driver.navigate.forward - expect(driver.current_url).to include(result_url) - expect(driver.title).to eq(result_title) - end + it 'refreshes the page' do + changed_title = 'Changed' - it 'refreshes the page' do - changed_title = 'Changed' + driver.navigate.to url_for('javascriptPage.html') + driver.find_element(link_text: 'Change the page title!').click + expect(driver.title).to eq(changed_title) - driver.navigate.to url_for('javascriptPage.html') - driver.find_element(link_text: 'Change the page title!').click - expect(driver.title).to eq(changed_title) + driver.navigate.refresh + wait.until { driver.title != changed_title } - driver.navigate.refresh - wait.until { driver.title != changed_title } - - expect(driver.title).to eq('Testing Javascript') - end -end + expect(driver.title).to eq('Testing Javascript') + end + end + end # WebDriver +end # Selenium