From c8b37e0910a4419760b1d69f4a1fab32d9d4074b Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 29 Jul 2026 12:37:33 -0500 Subject: [PATCH 01/21] [rb] scaffold BiDi protocol integration specs from schema --- rb/spec/BUILD.bazel | 1 + .../webdriver/bidi/protocol/BUILD.bazel | 23 + .../webdriver/bidi/protocol/bluetooth_spec.rb | 566 ++++++++++++++++++ .../webdriver/bidi/protocol/browser_spec.rb | 165 +++++ .../bidi/protocol/browsing_context_spec.rb | 343 +++++++++++ .../webdriver/bidi/protocol/emulation_spec.rb | 252 ++++++++ .../webdriver/bidi/protocol/input_spec.rb | 126 ++++ .../webdriver/bidi/protocol/network_spec.rb | 401 +++++++++++++ .../bidi/protocol/permissions_spec.rb | 101 ++++ .../webdriver/bidi/protocol/script_spec.rb | 190 ++++++ .../webdriver/bidi/protocol/session_spec.rb | 105 ++++ .../webdriver/bidi/protocol/storage_spec.rb | 154 +++++ .../protocol/user_agent_client_hints_spec.rb | 106 ++++ .../bidi/protocol/web_extension_spec.rb | 127 ++++ .../bidi/protocol_browsing_context_spec.rb | 46 +- 15 files changed, 2676 insertions(+), 30 deletions(-) create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb diff --git a/rb/spec/BUILD.bazel b/rb/spec/BUILD.bazel index aa42929e07a1d..ebcd6ad90e93b 100644 --- a/rb/spec/BUILD.bazel +++ b/rb/spec/BUILD.bazel @@ -18,6 +18,7 @@ rb_library( "//rb/spec/integration:all_srcs", "//rb/spec/integration/selenium/webdriver:all_srcs", "//rb/spec/integration/selenium/webdriver/bidi:all_srcs", + "//rb/spec/integration/selenium/webdriver/bidi/protocol:all_srcs", "//rb/spec/integration/selenium/webdriver/chrome:all_srcs", "//rb/spec/integration/selenium/webdriver/edge:all_srcs", "//rb/spec/integration/selenium/webdriver/firefox:all_srcs", diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel new file mode 100644 index 0000000000000..b913d90f09cd1 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel @@ -0,0 +1,23 @@ +load("//rb/spec:tests.bzl", "rb_integration_test") + +filegroup( + name = "all_srcs", + testonly = True, + srcs = glob(["*.rb"]), + visibility = ["//rb/spec:__pkg__"], +) + +[ + rb_integration_test( + name = file[:-8], + srcs = [file], + bidi = True, + classic = False, + data = ["//common/extensions"], + tags = ["exclusive-if-local"], + deps = [ + "//rb/lib/selenium/webdriver:bidi", + ], + ) + for file in glob(["*_spec.rb"]) +] diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb new file mode 100644 index 0000000000000..5a09b94c173c5 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -0,0 +1,566 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Bluetooth, + pending_if: [{browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /(?:Module bluetooth does not exist|bluetooth\.)/}, + reason: 'Firefox returns unknown command for BiDi Bluetooth simulation'}, + {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module bluetooth does not exist|bluetooth\.)/}, + reason: 'Safari returns unknown command for BiDi Bluetooth simulation'}], + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after do |example| + next if example.metadata[:skip] || example.skip + + begin + bluetooth.disable_simulation(context: driver.window_handle) + rescue StandardError + nil + end + reset_driver!(example: example) + end + + let(:bluetooth) { described_class.new(driver) } + let(:script) { Script.new(driver) } + let(:session) { Session.new(driver) } + + def target + Script::ContextTarget.new(context: driver.window_handle) + end + + def address + '00:11:22:33:44:55' + end + + def service_uuid + '0000180d-0000-1000-8000-00805f9b34fb' + end + + def characteristic_uuid + '00002a37-0000-1000-8000-00805f9b34fb' + end + + def descriptor_uuid + '00002902-0000-1000-8000-00805f9b34fb' + end + + def manufacturer_data + [Bluetooth::BluetoothManufacturerData.new(key: 1, data: 'AQID')] + end + + def scan_record + Bluetooth::ScanRecord.new( + name: 'Ruby Heart Rate', + uuids: [service_uuid], + appearance: 832, + manufacturer_data: manufacturer_data + ) + end + + def scan_entry + Bluetooth::SimulateAdvertisementScanEntryParameters.new( + device_address: address, + rssi: -60, + scan_record: scan_record + ) + end + + def enable_adapter + driver.navigate.to url_for('blank.html') + bluetooth.simulate_adapter(context: driver.window_handle, state: :powered_on, le_supported: true) + end + + def simulate_preconnected_device + enable_adapter + bluetooth.simulate_preconnected_peripheral( + context: driver.window_handle, + address: address, + name: 'Ruby Heart Rate', + manufacturer_data: manufacturer_data, + known_service_uuids: [service_uuid] + ) + end + + def subscribe(event) + events = [] + callback = driver.bidi.add_callback(event) { |params| events << params } + session.subscribe(events: [event]) + [events, callback] + end + + def unsubscribe(event, callback) + begin + session.unsubscribe(events: [event]) + rescue StandardError + nil + end + ensure + driver.bidi.remove_callback(event, callback) if callback + end + + def evaluate_value(expression, await_promise: false, user_activation: WebDriver::BiDi::Serialization::UNSET) + result = script.evaluate( + expression: expression, + target: target, + await_promise: await_promise, + user_activation: user_activation + ).result + result.respond_to?(:value) ? result.value : nil + end + + def start_request_device + evaluate_value( + <<~JS, + (() => { + window.__rubyBluetoothDevicePromise = navigator.bluetooth.requestDevice({ + filters: [{services: [#{service_uuid.inspect}]}] + }).then(device => { + window.__rubyBluetoothDevice = device; + return device.name; + }).catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + user_activation: true + ) + end + + def request_device_prompt(events) + wait.until do + events.find do |event| + Array(event['devices']).any? { |device| device['name'] == 'Ruby Heart Rate' } + end + end + end + + def advertised_device(prompt) + prompt['devices'].find { |device| device['name'] == 'Ruby Heart Rate' } + end + + def select_device + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + enable_adapter + start_request_device + + expect(bluetooth.simulate_advertisement( + context: driver.window_handle, + scan_entry: scan_entry + )).to be_empty + + prompt = request_device_prompt(events) + device = advertised_device(prompt) + expect(device['id']).to be_a(String) + + bluetooth.handle_request_device_prompt( + context: driver.window_handle, + prompt: prompt['prompt'], + accept: true, + device: device['id'] + ) + expect(evaluate_value('window.__rubyBluetoothDevicePromise', await_promise: true)).to eq('Ruby Heart Rate') + ensure + unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback + end + + def start_gatt_connection + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothGattPromise = window.__rubyBluetoothDevice.gatt.connect() + .then(server => server.connected) + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + end + + def connect_selected_device + events, callback = subscribe('bluetooth.gattConnectionAttempted') + start_gatt_connection + attempt = wait.until { events.find { |event| event['address'] == address } } + + expect(attempt['context']).to eq(driver.window_handle) + expect(bluetooth.simulate_gatt_connection_response( + context: driver.window_handle, + address: address, + code: 0 + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothGattPromise', await_promise: true)).to be(true) + ensure + unsubscribe('bluetooth.gattConnectionAttempted', callback) if callback + end + + def store_primary_service + evaluate_value( + <<~JS, + window.__rubyBluetoothDevice.gatt.getPrimaryService(#{service_uuid.inspect}) + .then(service => { + window.__rubyBluetoothService = service; + return service.uuid; + }) + JS + await_promise: true + ) + end + + def store_characteristic + evaluate_value( + <<~JS, + window.__rubyBluetoothService.getCharacteristic(#{characteristic_uuid.inspect}) + .then(characteristic => { + window.__rubyBluetoothCharacteristic = characteristic; + return characteristic.uuid; + }) + JS + await_promise: true + ) + end + + def store_descriptor + evaluate_value( + <<~JS, + window.__rubyBluetoothCharacteristic.getDescriptor(#{descriptor_uuid.inspect}) + .then(descriptor => { + window.__rubyBluetoothDescriptor = descriptor; + return descriptor.uuid; + }) + JS + await_promise: true + ) + end + + def add_service + bluetooth.simulate_service(context: driver.window_handle, address: address, uuid: service_uuid, type: :add) + end + + def add_characteristic + properties = Bluetooth::CharacteristicProperties.new(read: true, write: true, notify: true) + bluetooth.simulate_characteristic( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :add, + characteristic_properties: properties + ) + end + + def add_descriptor + bluetooth.simulate_descriptor( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :add + ) + end + + describe '#handle_request_device_prompt' do + it 'accepts a prompt' do + select_device + + expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') + end + + it 'cancels a prompt' do + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + enable_adapter + start_request_device + + expect(bluetooth.simulate_advertisement( + context: driver.window_handle, + scan_entry: scan_entry + )).to be_empty + + prompt = request_device_prompt(events) + bluetooth.handle_request_device_prompt( + context: driver.window_handle, + prompt: prompt['prompt'], + accept: false + ) + expect(evaluate_value('window.__rubyBluetoothDevicePromise', await_promise: true)).to start_with('ERROR:') + ensure + unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback + end + end + + describe '#simulate_adapter' do + it 'simulates a powered-on adapter' do + expect(enable_adapter).to be_empty + end + end + + describe '#disable_simulation' do + it 'disables active Bluetooth simulation' do + enable_adapter + + expect(bluetooth.disable_simulation(context: driver.window_handle)).to be_empty + end + end + + describe '#simulate_preconnected_peripheral' do + it 'simulates a preconnected peripheral' do + expect(simulate_preconnected_device).to be_empty + end + end + + describe '#simulate_advertisement' do + it 'simulates an advertisement scan entry' do + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + enable_adapter + start_request_device + + expect(bluetooth.simulate_advertisement( + context: driver.window_handle, + scan_entry: scan_entry + )).to be_empty + + prompt = request_device_prompt(events) + expect(advertised_device(prompt)['id']).to be_a(String) + ensure + unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback + end + end + + describe '#simulate_gatt_connection_response' do + it 'simulates a successful GATT connection response' do + select_device + + connect_selected_device + expect(evaluate_value('window.__rubyBluetoothDevice.gatt.connected')).to be(true) + end + end + + describe '#simulate_gatt_disconnection' do + it 'simulates a GATT disconnection' do + select_device + connect_selected_device + + expect(bluetooth.simulate_gatt_disconnection( + context: driver.window_handle, + address: address + )).to be_empty + wait.until { evaluate_value('window.__rubyBluetoothDevice.gatt.connected') == false } + end + end + + describe '#simulate_service' do + it 'adds and removes a simulated service' do + simulate_preconnected_device + + expect(bluetooth.simulate_service( + context: driver.window_handle, + address: address, + uuid: service_uuid, + type: :add + )).to be_empty + expect(bluetooth.simulate_service( + context: driver.window_handle, + address: address, + uuid: service_uuid, + type: :remove + )).to be_empty + end + end + + describe '#simulate_characteristic' do + it 'adds and removes a characteristic with properties' do + simulate_preconnected_device + add_service + properties = Bluetooth::CharacteristicProperties.new(read: true, write: true, notify: true) + + expect(bluetooth.simulate_characteristic( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :add, + characteristic_properties: properties + )).to be_empty + expect(bluetooth.simulate_characteristic( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :remove + )).to be_empty + end + end + + describe '#simulate_characteristic_response' do + it 'simulates characteristic read and write responses' do + select_device + add_service + add_characteristic + connect_selected_device + store_primary_service + store_characteristic + + events, callback = subscribe('bluetooth.characteristicEventGenerated') + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothReadPromise = window.__rubyBluetoothCharacteristic.readValue() + .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'read' } } + expect(bluetooth.simulate_characteristic_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :read, + code: 0, + data: [1, 2, 3] + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothReadPromise', await_promise: true)).to eq('1,2,3') + + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothWritePromise = window.__rubyBluetoothCharacteristic + .writeValueWithResponse(new Uint8Array([4, 5])) + .then(() => 'written') + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'write-with-response' } } + expect(bluetooth.simulate_characteristic_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :write, + code: 0 + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothWritePromise', await_promise: true)).to eq('written') + ensure + unsubscribe('bluetooth.characteristicEventGenerated', callback) if callback + end + end + + describe '#simulate_descriptor' do + it 'adds and removes a descriptor' do + simulate_preconnected_device + add_service + add_characteristic + + expect(bluetooth.simulate_descriptor( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :add + )).to be_empty + expect(bluetooth.simulate_descriptor( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :remove + )).to be_empty + end + end + + describe '#simulate_descriptor_response' do + it 'simulates descriptor read and write responses' do + select_device + add_service + add_characteristic + add_descriptor + connect_selected_device + store_primary_service + store_characteristic + store_descriptor + + events, callback = subscribe('bluetooth.descriptorEventGenerated') + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothDescriptorReadPromise = window.__rubyBluetoothDescriptor.readValue() + .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'read' } } + expect(bluetooth.simulate_descriptor_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :read, + code: 0, + data: [1, 2] + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothDescriptorReadPromise', await_promise: true)).to eq('1,2') + + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothDescriptorWritePromise = window.__rubyBluetoothDescriptor + .writeValue(new Uint8Array([6, 7])) + .then(() => 'written') + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'write' } } + expect(bluetooth.simulate_descriptor_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :write, + code: 0 + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothDescriptorWritePromise', + await_promise: true)).to eq('written') + ensure + unsubscribe('bluetooth.descriptorEventGenerated', callback) if callback + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb new file mode 100644 index 0000000000000..74df9e9da4223 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb @@ -0,0 +1,165 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' +require 'tmpdir' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Browser, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:browser) { described_class.new(driver) } + + def create_user_context + browser.create_user_context.user_context + end + + context 'with user contexts and client windows', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module browser does not exist|browser\.)/}, + reason: 'Safari returns unknown command for BiDi user contexts/client windows'} do + describe '#create_user_context' do + it 'returns the created user context id' do + user_context = create_user_context + + expect(user_context).to be_a(String) + expect(browser.get_user_contexts.user_contexts.map(&:user_context)).to include(user_context) + ensure + browser.remove_user_context(user_context: user_context) if user_context + end + + it 'accepts optional proxy and prompt behavior parameters' do + user_context = browser.create_user_context( + accept_insecure_certs: true, + proxy: Session::DirectProxyConfiguration.new, + unhandled_prompt_behavior: Session::UserPromptHandler.new(default: :dismiss) + ).user_context + + expect(user_context).to be_a(String) + ensure + browser.remove_user_context(user_context: user_context) if user_context + end + end + + describe '#get_client_windows' do + it 'returns typed client window information' do + windows = browser.get_client_windows.client_windows + + expect(windows).not_to be_empty + expect(windows.first).to be_a(Browser::ClientWindowInfo) + expect(windows.first.client_window).to be_a(String) + expect(windows.first.active).to be(true).or be(false) + expect(windows.first.width).to be_positive + expect(windows.first.height).to be_positive + expect(%i[fullscreen maximized minimized normal].include?(windows.first.state)).to be(true) + end + end + + describe '#get_user_contexts' do + it 'includes newly created user contexts' do + user_contexts = Array.new(2) { create_user_context } + all_ids = browser.get_user_contexts.user_contexts.map(&:user_context) + + expect(all_ids).to include(*user_contexts) + ensure + user_contexts&.each { |id| browser.remove_user_context(user_context: id) } + end + end + + describe '#remove_user_context' do + it 'removes the requested user context' do + user_context = create_user_context + + browser.remove_user_context(user_context: user_context) + + expect(browser.get_user_contexts.user_contexts.map(&:user_context)).not_to include(user_context) + end + end + + describe '#set_client_window_state' do + it 'sets a client window to a normal rectangle' do + client_window = browser.get_client_windows.client_windows.first.client_window + + result = browser.set_client_window_state( + client_window: client_window, + state: :normal, + width: 640, + height: 480, + x: 10, + y: 10 + ) + + expect(result).to be_a(Browser::ClientWindowInfo) + expect(result.client_window).to eq(client_window) + expect(result.state).to eq(:normal) + expect(result.width).to be >= 320 + expect(result.height).to be >= 240 + end + end + + describe '#set_download_behavior' do + it 'allows downloads into a requested folder' do + Dir.mktmpdir('selenium-bidi-downloads') do |directory| + behavior = Browser::DownloadBehavior::Allowed.new(destination_folder: directory) + browser.set_download_behavior(download_behavior: behavior) + + driver.navigate.to url_for('downloads/download.html') + driver.find_element(id: 'file-1').click + + wait.until { Dir.children(directory).any? { |file| file.start_with?('file_1') } } + expect(Dir.children(directory)).to include(a_string_matching(/^file_1.*\.txt$/)) + ensure + browser.set_download_behavior(download_behavior: nil) + end + end + + it 'accepts a user-context scoped download behavior' do + user_context = create_user_context + behavior = Browser::DownloadBehavior::Denied.new + + expect(browser.set_download_behavior(download_behavior: behavior, + user_contexts: [user_context])).to be_empty + ensure + browser.set_download_behavior(download_behavior: nil, user_contexts: [user_context]) if user_context + browser.remove_user_context(user_context: user_context) if user_context + end + end + end + + describe '#close', + pending_if: {browser: :firefox, + exception: {class: Error::UnsupportedOperationError, + message: /Closing the browser in a session /}, + reason: 'Firefox unsupported operation for browser.close on Classic sessions'} do + it 'closes the browser session' do + driver.navigate.to url_for('blank.html') + + expect(browser.close).to be_empty + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb new file mode 100644 index 0000000000000..cb6c151d1591c --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -0,0 +1,343 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe BrowsingContext, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:browsing_context) { described_class.new(driver) } + + def script_target(context = driver.window_handle, sandbox: nil) + kwargs = {context: context} + kwargs[:sandbox] = sandbox if sandbox + Script::ContextTarget.new(**kwargs) + end + + def evaluate(expression, context = driver.window_handle) + Script.new(driver).evaluate(expression: expression, target: script_target(context), await_promise: false) + end + + describe '#activate' do + it 'activates an existing browsing context' do + context = browsing_context.create(type: :tab, background: true).context + + expect(browsing_context.activate(context: context)).to be_empty + expect(driver.window_handles).to include(context) + end + end + + describe '#capture_screenshot' do + it 'returns base64 PNG screenshot data' do + browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) + + result = browsing_context.capture_screenshot(context: driver.window_handle) + + expect(result).to be_a(BrowsingContext::CaptureScreenshotResult) + expect(result.data).to start_with('iVBOR') + end + + it 'accepts viewport origin, image format, and box clip parameters' do + browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) + + result = browsing_context.capture_screenshot( + context: driver.window_handle, + origin: :viewport, + format: BrowsingContext::ImageFormat.new(type: 'image/png'), + clip: BrowsingContext::BoxClipRectangle.new(x: 0, y: 0, width: 100, height: 100) + ) + + expect(result.data).to start_with('iVBOR') + end + end + + describe '#close' do + it 'closes a created tab' do + context = browsing_context.create(type: :tab).context + + browsing_context.close(context: context) + + expect(driver.window_handles).not_to include(context) + end + + it 'accepts the prompt unload option' do + context = browsing_context.create(type: :tab).context + + expect(browsing_context.close(context: context, prompt_unload: false)).to be_empty + expect(driver.window_handles).not_to include(context) + end + end + + describe '#create' do + it 'creates a tab context' do + result = browsing_context.create(type: :tab) + + expect(result).to be_a(BrowsingContext::CreateResult) + expect(driver.window_handles).to include(result.context) + end + + it 'creates a window context' do + result = browsing_context.create(type: :window) + + expect(result.context).to be_a(String) + expect(driver.window_handles).to include(result.context) + end + + it 'accepts reference, background, and user context parameters', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module browser does not exist|browser\.|browsingContext)/}, + reason: 'Safari driver currently returns unknown command for BiDi user contexts'} do + user_context = Browser.new(driver).create_user_context.user_context + + result = browsing_context.create( + type: :tab, + reference_context: driver.window_handle, + background: true, + user_context: user_context + ) + + expect(driver.window_handles).to include(result.context) + expect(browsing_context.get_tree(root: result.context).contexts.first.user_context).to eq(user_context) + ensure + browsing_context.close(context: result.context) if result&.context + Browser.new(driver).remove_user_context(user_context: user_context) if user_context + end + end + + describe '#get_tree' do + it 'returns the current context tree' do + browsing_context.navigate(context: driver.window_handle, url: url_for('iframes.html'), wait: :complete) + + result = browsing_context.get_tree(root: driver.window_handle) + + expect(result.contexts).to contain_exactly(be_a(BrowsingContext::Info)) + expect(result.contexts.first.context).to eq(driver.window_handle) + expect(result.contexts.first.url).to include('iframes.html') + expect(result.contexts.first.children).not_to be_empty + end + + it 'accepts max depth and root parameters' do + browsing_context.navigate(context: driver.window_handle, url: url_for('iframes.html'), wait: :complete) + + result = browsing_context.get_tree(max_depth: 0, root: driver.window_handle) + + expect(result.contexts.size).to eq(1) + expect(result.contexts.first.context).to eq(driver.window_handle) + expect(result.contexts.first.children).to be_nil.or be_empty + end + end + + describe '#handle_user_prompt' do + it 'returns a no such alert error when no prompt is open' do + expect { + browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) + }.to raise_error(Error::NoSuchAlertError) + end + + it 'accepts user text in the protocol payload' do + expect { + browsing_context.handle_user_prompt( + context: driver.window_handle, + accept: true, + user_text: 'Ruby BiDi' + ) + }.to raise_error(Error::NoSuchAlertError) + end + end + + describe '#locate_nodes' do + it 'finds nodes by CSS selector' do + browsing_context.navigate(context: driver.window_handle, url: url_for('xhtmlTest.html'), wait: :complete) + + result = browsing_context.locate_nodes( + context: driver.window_handle, + locator: BrowsingContext::CssLocator.new(value: 'div.content'), + max_node_count: 1 + ) + + expect(result.nodes).to contain_exactly(be_a(Script::NodeRemoteValue)) + expect(result.nodes.first.value.local_name).to eq('div') + expect(result.nodes.first.value.attributes).to include('class' => 'content') + expect(result.nodes.first.shared_id).to be_a(String) + end + + it 'accepts serialization options and start nodes' do + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + forms = browsing_context.locate_nodes( + context: driver.window_handle, + locator: BrowsingContext::CssLocator.new(value: 'form'), + max_node_count: 1 + ) + start_node = Script::SharedReference.new(shared_id: forms.nodes.first.shared_id) + + result = browsing_context.locate_nodes( + context: driver.window_handle, + locator: BrowsingContext::CssLocator.new(value: 'input'), + max_node_count: 3, + serialization_options: Script::SerializationOptions.new(max_dom_depth: 0), + start_nodes: [start_node] + ) + + expect(result.nodes.size).to eq(3) + expect(result.nodes.map { |node| node.value.local_name }.uniq).to eq(['input']) + end + end + + describe '#navigate' do + it 'navigates a context to a URL' do + result = browsing_context.navigate( + context: driver.window_handle, + url: url_for('formPage.html'), + wait: :complete + ) + + expect(result).to be_a(BrowsingContext::NavigateResult) + expect(result.url).to eq(url_for('formPage.html')) + expect(driver.find_element(name: 'login')).to be_displayed + end + end + + describe '#print' do + it 'returns base64 PDF data' do + browsing_context.navigate(context: driver.window_handle, url: url_for('printPage.html'), wait: :complete) + + result = browsing_context.print(context: driver.window_handle) + + expect(result).to be_a(BrowsingContext::PrintResult) + expect(result.data).to start_with('JVBER') + end + + it 'accepts page layout parameters' do + browsing_context.navigate(context: driver.window_handle, url: url_for('printPage.html'), wait: :complete) + + result = browsing_context.print( + context: driver.window_handle, + background: true, + margin: BrowsingContext::PrintMarginParameters.new(top: 0.5, bottom: 0.5, left: 0.5, right: 0.5), + orientation: :landscape, + page: BrowsingContext::PrintPageParameters.new(width: 8.5, height: 11), + page_ranges: ['1'], + scale: 1.0, + shrink_to_fit: true + ) + + expect(result.data).to start_with('JVBER') + end + end + + describe '#reload' do + it 'reloads a context', + pending_if: {browser: :firefox, + exception: {class: Error::UnsupportedOperationError, + message: /Argument "ignoreCache" /}, + reason: 'Firefox lacks browsingContext.reload ignoreCache (bugzilla 1851561)'} do + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + + result = browsing_context.reload(context: driver.window_handle, ignore_cache: true, wait: :complete) + + expect(result.url).to eq(url_for('formPage.html')) + expect(driver.find_element(name: 'login')).to be_displayed + end + end + + describe '#set_bypass_csp', + pending_if: [{browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /browsingContext\.setBypassCSP/}, + reason: 'Chromium returns unsupported operation for browsingContext.setBypassCSP'}, + {browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /browsingContext\.setBypassCSP/}, + reason: 'Firefox returns unknown command for browsingContext.setBypassCSP'}] do + it 'sets and clears CSP bypass for a context' do + expect(browsing_context.set_bypass_csp(bypass: true, contexts: [driver.window_handle])).to be_empty + expect(browsing_context.set_bypass_csp(bypass: nil, contexts: [driver.window_handle])).to be_empty + end + end + + describe '#set_viewport' do + it 'sets the viewport size and device pixel ratio' do + browsing_context.set_viewport( + context: driver.window_handle, + viewport: BrowsingContext::Viewport.new(width: 800, height: 600), + device_pixel_ratio: 2.0 + ) + + expect(evaluate('[window.innerWidth, window.innerHeight]').result.value.map(&:value)).to eq([800, 600]) + end + + it 'clears the viewport override' do + browsing_context.set_viewport( + context: driver.window_handle, + viewport: BrowsingContext::Viewport.new(width: 640, height: 480) + ) + + expect(browsing_context.set_viewport(context: driver.window_handle, viewport: nil)).to be_empty + end + end + + describe '#start_screencast', + pending_if: {browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /browsingContext\.startScreencast/}, + reason: 'Chromium returns unsupported operation for browsingContext.startScreencast'} do + it 'starts and stops a screencast' do + result = browsing_context.start_screencast( + context: driver.window_handle, + mime_type: 'video/webm', + video: BrowsingContext::MediaTrackConstraints.new(width: 320, height: 240, frame_rate: 5), + audio: false + ) + + expect(result.screencast).to be_a(String) + expect(browsing_context.stop_screencast(screencast: result.screencast)).to be_a( + BrowsingContext::StopScreencastResult + ) + end + end + + describe '#traverse_history' do + it 'moves backward and forward in the context history' do + browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) + browsing_context.navigate( + context: driver.window_handle, + url: url_for('bidi/logEntryAdded.html'), + wait: :complete + ) + + browsing_context.traverse_history(context: driver.window_handle, delta: -1) + wait_for_url('blank.html') + expect(driver.current_url).to include('blank.html') + + browsing_context.traverse_history(context: driver.window_handle, delta: 1) + wait_for_url('bidi/logEntryAdded.html') + expect(driver.current_url).to include('bidi/logEntryAdded.html') + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb new file mode 100644 index 0000000000000..b6fe103dd3b4d --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb @@ -0,0 +1,252 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Emulation, + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module emulation does not exist|emulation\.)/}, + reason: 'Safari driver currently returns unknown command for BiDi emulation commands'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:emulation) { described_class.new(driver) } + let(:script) { Script.new(driver) } + let(:browsing_context) { BrowsingContext.new(driver) } + + def target(context) + Script::ContextTarget.new(context: context) + end + + def evaluate_value(expression, context = driver.window_handle) + result = script.evaluate(expression: expression, target: target(context), await_promise: false).result + result.respond_to?(:value) ? result.value : nil + end + + def create_context + browsing_context.create(type: :tab).context + end + + describe '#set_forced_colors_mode_theme_override', + pending_if: [{browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /emulation\.setForcedColorsModeThemeOverride/}, + reason: 'Chromium unsupported operation: emulation.setForcedColorsModeThemeOverride'}, + {browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /emulation\.setForcedColorsModeThemeOverride/}, + reason: 'Firefox unknown command: emulation.setForcedColorsModeThemeOverride'}] do + it 'sets and clears forced-colors theme override' do + expect(emulation.set_forced_colors_mode_theme_override( + theme: :dark, + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_forced_colors_mode_theme_override( + theme: nil, + contexts: [driver.window_handle] + )).to be_empty + end + end + + describe '#set_geolocation_override' do + it 'sets and clears geolocation coordinates' do + coordinates = Emulation::GeolocationCoordinates.new(latitude: 37, longitude: -122, accuracy: 10) + + expect(emulation.set_geolocation_override( + coordinates: coordinates, + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_geolocation_override( + coordinates: nil, + contexts: [driver.window_handle] + )).to be_empty + end + + it 'sets a geolocation error override' do + expect(emulation.set_geolocation_override( + error: Emulation::GeolocationPositionError.new, + contexts: [driver.window_handle] + )).to be_empty + ensure + begin + emulation.set_geolocation_override(coordinates: nil, contexts: [driver.window_handle]) + rescue StandardError + nil + end + end + end + + describe '#set_locale_override' do + it 'overrides locale for a fresh context' do + context = create_context + + emulation.set_locale_override(locale: 'fr-FR', contexts: [context]) + browsing_context.navigate(context: context, url: url_for('blank.html'), wait: :complete) + + expect(evaluate_value('Intl.DateTimeFormat().resolvedOptions().locale', context)).to start_with('fr') + ensure + emulation.set_locale_override(locale: nil, contexts: [context]) if context + browsing_context.close(context: context) if context + end + end + + describe '#set_network_conditions' do + it 'sets and clears offline network conditions' do + expect(emulation.set_network_conditions( + network_conditions: Emulation::NetworkConditionsOffline.new, + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_network_conditions( + network_conditions: nil, + contexts: [driver.window_handle] + )).to be_empty + end + end + + describe '#set_screen_orientation_override' do + it 'sets and clears screen orientation' do + orientation = Emulation::ScreenOrientation.new(natural: :portrait, type: :portrait_primary) + + expect(emulation.set_screen_orientation_override( + screen_orientation: orientation, + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_screen_orientation_override( + screen_orientation: nil, + contexts: [driver.window_handle] + )).to be_empty + end + end + + describe '#set_screen_settings_override' do + it 'sets and clears screen area' do + expect(emulation.set_screen_settings_override( + screen_area: Emulation::ScreenArea.new(width: 800, height: 600), + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_screen_settings_override( + screen_area: nil, + contexts: [driver.window_handle] + )).to be_empty + end + end + + describe '#set_scripting_enabled', + pending_if: {browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /emulation\.setScriptingEnabled/}, + reason: 'Firefox returns unknown command for emulation.setScriptingEnabled'} do + it 'disables and restores page scripting' do + context = driver.window_handle + + emulation.set_scripting_enabled(enabled: false, contexts: [context]) + browsing_context.navigate( + context: context, + url: "data:text/html,", + wait: :complete + ) + expect(evaluate_value('window.hello', context)).to be_nil + + emulation.set_scripting_enabled(enabled: nil, contexts: [context]) + browsing_context.navigate( + context: context, + url: "data:text/html,", + wait: :complete + ) + expect(evaluate_value('window.hello', context)).to eq('World') + ensure + begin + emulation.set_scripting_enabled(enabled: nil, contexts: [driver.window_handle]) + rescue StandardError + nil + end + end + end + + describe '#set_scrollbar_type_override', + pending_if: {browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /emulation\.setScrollbarTypeOverride/}, + reason: 'Firefox returns unknown command for emulation.setScrollbarTypeOverride'} do + it 'sets and clears scrollbar type override' do + expect(emulation.set_scrollbar_type_override( + scrollbar_type: :classic, + contexts: [driver.window_handle] + )).to be_empty + expect(emulation.set_scrollbar_type_override( + scrollbar_type: nil, + contexts: [driver.window_handle] + )).to be_empty + end + end + + describe '#set_timezone_override' do + it 'overrides timezone for a fresh context' do + context = create_context + + emulation.set_timezone_override(timezone: 'UTC', contexts: [context]) + browsing_context.navigate(context: context, url: url_for('blank.html'), wait: :complete) + + expect(evaluate_value('Intl.DateTimeFormat().resolvedOptions().timeZone', context)).to eq('UTC') + ensure + emulation.set_timezone_override(timezone: nil, contexts: [context]) if context + browsing_context.close(context: context) if context + end + end + + describe '#set_touch_override', + pending_if: {browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /emulation\.setTouchOverride/}, + reason: 'Firefox returns unknown command for emulation.setTouchOverride'} do + it 'sets and clears touch support' do + context = driver.window_handle + + emulation.set_touch_override(max_touch_points: 5, contexts: [context]) + expect(evaluate_value('navigator.maxTouchPoints', context)).to eq(5) + + expect(emulation.set_touch_override(max_touch_points: nil, contexts: [context])).to be_empty + end + end + + describe '#set_user_agent_override' do + it 'overrides and clears the user agent' do + context = driver.window_handle + custom_user_agent = 'Ruby BiDi UA/1.0' + + emulation.set_user_agent_override(user_agent: custom_user_agent, contexts: [context]) + browsing_context.navigate(context: context, url: url_for('blank.html'), wait: :complete) + expect(evaluate_value('navigator.userAgent', context)).to eq(custom_user_agent) + + emulation.set_user_agent_override(user_agent: nil, contexts: [context]) + browsing_context.navigate(context: context, url: url_for('blank.html'), wait: :complete) + expect(evaluate_value('navigator.userAgent', context)).not_to eq(custom_user_agent) + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb new file mode 100644 index 0000000000000..44b1b1f0463dd --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb @@ -0,0 +1,126 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Input, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:input) { described_class.new(driver) } + let(:script) { Script.new(driver) } + + def target + Script::ContextTarget.new(context: driver.window_handle) + end + + def shared_reference(selector) + result = script.evaluate( + expression: "document.querySelector(#{selector.inspect})", + target: target, + await_promise: false, + result_ownership: :root + ) + Script::SharedReference.new(shared_id: result.result.shared_id, handle: result.result.handle) + end + + describe '#perform_actions' do + it 'clicks an element with pointer actions' do + driver.navigate.to url_for('javascriptPage.html') + element = shared_reference('#clickField') + + input.perform_actions( + context: driver.window_handle, + actions: [ + Input::PointerSourceActions.new( + id: 'mouse', + parameters: Input::PointerParameters.new(pointer_type: :mouse), + actions: [ + Input::PointerMoveAction.new( + x: 0, + y: 0, + origin: Input::ElementOrigin.new(element: element) + ), + Input::PointerDownAction.new(button: 0), + Input::PointerUpAction.new(button: 0) + ] + ) + ] + ) + + expect(driver.find_element(id: 'clickField').attribute('value')).to eq('Clicked') + end + end + + describe '#release_actions' do + it 'releases active input sources' do + driver.navigate.to url_for('javascriptPage.html') + element = shared_reference('#clickField') + + input.perform_actions( + context: driver.window_handle, + actions: [ + Input::PointerSourceActions.new( + id: 'mouse', + parameters: Input::PointerParameters.new(pointer_type: :mouse), + actions: [ + Input::PointerMoveAction.new(x: 0, y: 0, origin: Input::ElementOrigin.new(element: element)), + Input::PointerDownAction.new(button: 0) + ] + ) + ] + ) + + expect(input.release_actions(context: driver.window_handle)).to be_empty + end + end + + describe '#set_files', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module input does not exist|input\.setFiles)/}, + reason: 'Safari driver currently returns an error for input.setFiles'} do + it 'sets files on a file input element' do + file = create_tempfile + driver.navigate.to url_for('upload.html') + element = shared_reference('#upload') + + expect(driver.find_element(id: 'upload').attribute('value')).to be_empty + + input.set_files(context: driver.window_handle, element: element, files: [file.path]) + + expect(driver.find_element(id: 'upload').attribute('value')).not_to be_empty + driver.find_element(id: 'go').click + wait.until { driver.find_element(id: 'upload_label').displayed? } + driver.switch_to.frame('upload_target') + expect(driver.find_element(tag_name: 'body').text).to include('This is a dummy test file') + ensure + file&.close + file&.unlink + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb new file mode 100644 index 0000000000000..4dbb244af601b --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb @@ -0,0 +1,401 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Network, + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module network does not exist|network\.)/}, + reason: 'Safari driver currently returns unknown command for BiDi network commands'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:network) { described_class.new(driver) } + let(:browsing_context) { BrowsingContext.new(driver) } + let(:session) { Session.new(driver) } + + def bytes(value) + Network::StringValue.new(value: value) + end + + def header(name, value) + Network::Header.new(name: name, value: bytes(value)) + end + + def subscribe(event) + events = [] + callback = driver.bidi.add_callback(event) { |params| events << params } + session.subscribe(events: [event]) + [events, callback] + end + + def unsubscribe(event, callback) + session.unsubscribe(events: [event]) + ensure + driver.bidi.remove_callback(event, callback) if callback + end + + def blocked_event(events, intercept) + wait.until do + events.find { |event| event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) } + end + end + + describe '#add_data_collector' do + it 'returns a collector id' do + result = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) + + expect(result).to be_a(Network::AddDataCollectorResult) + expect(result.collector).to be_a(String) + ensure + network.remove_data_collector(collector: result.collector) if result&.collector + end + + it 'accepts collector type and context filters' do + result = network.add_data_collector( + data_types: [:request], + max_encoded_data_size: 200_000_000, + collector_type: :blob, + contexts: [driver.window_handle] + ) + + expect(result.collector).to be_a(String) + ensure + network.remove_data_collector(collector: result.collector) if result&.collector + end + end + + describe '#add_intercept' do + it 'returns an intercept id' do + result = network.add_intercept(phases: [:before_request_sent]) + + expect(result).to be_a(Network::AddInterceptResult) + expect(result.intercept).to be_a(String) + ensure + network.remove_intercept(intercept: result.intercept) if result&.intercept + end + + it 'accepts context and URL pattern filters' do + result = network.add_intercept( + phases: [:before_request_sent], + contexts: [driver.window_handle], + url_patterns: [Network::UrlPatternString.new(pattern: url_for('formPage.html'))] + ) + + expect(result.intercept).to be_a(String) + ensure + network.remove_intercept(intercept: result.intercept) if result&.intercept + end + end + + describe '#continue_request' do + it 'continues an intercepted request' do + intercept = network.add_intercept(phases: [:before_request_sent]) + events, callback = subscribe('network.beforeRequestSent') + + driver.bidi.add_callback('network.beforeRequestSent') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.continue_request(request: event['request']['request']) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + + expect(events.count { |event| event['isBlocked'] }).to be_positive + expect(driver.find_element(name: 'login')).to be_displayed + ensure + unsubscribe('network.beforeRequestSent', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + + it 'accepts optional headers, method, and URL parameters' do + intercept = network.add_intercept( + phases: [:before_request_sent], + url_patterns: [Network::UrlPatternString.new(pattern: url_for('bidi/emptyText.txt'))] + ) + events, callback = subscribe('network.beforeRequestSent') + + driver.bidi.add_callback('network.beforeRequestSent') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.continue_request( + request: event['request']['request'], + headers: [header('x-ruby-bidi', 'continued')], + method_: 'GET', + url: event['request']['url'] + ) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate( + context: driver.window_handle, + url: url_for('bidi/emptyText.txt'), + wait: :complete + ) + + expect(blocked_event(events, intercept)['request']['url']).to eq(url_for('bidi/emptyText.txt')) + ensure + unsubscribe('network.beforeRequestSent', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + end + + describe '#continue_response' do + it 'continues an intercepted response' do + intercept = network.add_intercept(phases: [:response_started]) + events, callback = subscribe('network.responseStarted') + + driver.bidi.add_callback('network.responseStarted') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.continue_response(request: event['request']['request']) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + + expect(blocked_event(events, intercept)['response']['status']).to eq(200) + expect(driver.find_element(name: 'login')).to be_displayed + ensure + unsubscribe('network.responseStarted', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + + it 'accepts optional response headers and status parameters' do + intercept = network.add_intercept(phases: [:response_started]) + events, callback = subscribe('network.responseStarted') + + driver.bidi.add_callback('network.responseStarted') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.continue_response( + request: event['request']['request'], + headers: [header('x-ruby-bidi-response', 'continued')], + reason_phrase: 'OK', + status_code: 200 + ) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + + expect(blocked_event(events, intercept)['response']['status']).to eq(200) + ensure + unsubscribe('network.responseStarted', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + end + + describe '#continue_with_auth' do + it 'provides credentials for an auth challenge' do + username, password = SpecSupport::RackServer::TestApp::BASIC_AUTH_CREDENTIALS + intercept = network.add_intercept(phases: [:auth_required]) + _events, callback = subscribe('network.authRequired') + + driver.bidi.add_callback('network.authRequired') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.continue_with_auth( + request: event['request']['request'], + action: :provide_credentials, + credentials: Network::AuthCredentials.new(username: username, password: password) + ) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate(context: driver.window_handle, url: url_for('basicAuth'), wait: :complete) + + expect(driver.find_element(tag_name: 'h1').text).to eq('authorized') + ensure + unsubscribe('network.authRequired', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + end + + describe '#fail_request' do + it 'fails an intercepted request' do + intercept = network.add_intercept( + phases: [:before_request_sent], + url_patterns: [Network::UrlPatternString.new(pattern: url_for('formPage.html'))] + ) + _events, callback = subscribe('network.beforeRequestSent') + + driver.bidi.add_callback('network.beforeRequestSent') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.fail_request(request: event['request']['request']) + rescue Error::WebDriverError + nil + end + + expect { + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + }.to raise_error(Error::WebDriverError) + ensure + unsubscribe('network.beforeRequestSent', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + end + + describe '#get_data' do + it 'gets and disowns collected response data' do + collector = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) + events, callback = subscribe('network.responseCompleted') + + browsing_context.navigate( + context: driver.window_handle, + url: url_for('bidi/emptyText.txt'), + wait: :complete + ) + event = wait.until { events.find { |item| item.dig('request', 'url') == url_for('bidi/emptyText.txt') } } + + result = network.get_data( + data_type: :response, + collector: collector.collector, + request: event['request']['request'] + ) + + expect(result.bytes).to be_a(Network::StringValue).or be_a(Network::Base64Value) + expect(network.disown_data( + data_type: :response, + collector: collector.collector, + request: event['request']['request'] + )).to be_empty + ensure + unsubscribe('network.responseCompleted', callback) if callback + network.remove_data_collector(collector: collector.collector) if collector&.collector + end + end + + describe '#provide_response' do + it 'provides a complete response body' do + intercept = network.add_intercept( + phases: [:before_request_sent], + url_patterns: [Network::UrlPatternString.new(pattern: url_for('formPage.html'))] + ) + _events, callback = subscribe('network.beforeRequestSent') + + driver.bidi.add_callback('network.beforeRequestSent') do |event| + next unless event['isBlocked'] && Array(event['intercepts']).include?(intercept.intercept) + + network.provide_response( + request: event['request']['request'], + status_code: 200, + reason_phrase: 'OK', + headers: [header('content-type', 'text/html')], + body: bytes('Provided by Ruby BiDiok') + ) + rescue Error::WebDriverError + nil + end + + browsing_context.navigate(context: driver.window_handle, url: url_for('formPage.html'), wait: :complete) + + expect(driver.title).to eq('Provided by Ruby BiDi') + ensure + unsubscribe('network.beforeRequestSent', callback) if callback + network.remove_intercept(intercept: intercept.intercept) if intercept&.intercept + end + end + + describe '#remove_data_collector' do + it 'removes a collector' do + collector = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) + + expect(network.remove_data_collector(collector: collector.collector)).to be_empty + end + end + + describe '#remove_intercept' do + it 'removes an intercept' do + intercept = network.add_intercept(phases: [:before_request_sent]) + + expect(network.remove_intercept(intercept: intercept.intercept)).to be_empty + end + end + + describe '#set_cache_behavior' do + it 'sets and clears context cache behavior' do + expect(network.set_cache_behavior(cache_behavior: :bypass, contexts: [driver.window_handle])).to be_empty + expect(network.set_cache_behavior(cache_behavior: :default, contexts: [driver.window_handle])).to be_empty + end + end + + describe '#set_extra_headers' do + it 'adds extra request headers for a context' do + events, callback = subscribe('network.beforeRequestSent') + + network.set_extra_headers(headers: [header('x-ruby-bidi-extra', 'present')], + contexts: [driver.window_handle]) + browsing_context.navigate( + context: driver.window_handle, + url: url_for('bidi/emptyText.txt'), + wait: :complete + ) + + event = wait.until do + events.find do |item| + Array(item.dig('request', 'headers')).any? do |item_header| + item_header['name'].casecmp?('x-ruby-bidi-extra') + end + end + end + actual = event['request']['headers'].find do |item_header| + item_header['name'].casecmp?('x-ruby-bidi-extra') + end + + expect(actual['value']['value']).to eq('present') + ensure + begin + network.set_extra_headers(headers: [], contexts: [driver.window_handle]) + rescue StandardError + nil + end + unsubscribe('network.beforeRequestSent', callback) if callback + end + + it 'accepts user-context filters' do + user_context = Browser.new(driver).create_user_context.user_context + + expect(network.set_extra_headers( + headers: [header('x-ruby-bidi-user-context', 'present')], + user_contexts: [user_context] + )).to be_empty + ensure + network.set_extra_headers(headers: [], user_contexts: [user_context]) if user_context + Browser.new(driver).remove_user_context(user_context: user_context) if user_context + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb new file mode 100644 index 0000000000000..c3ca1ce106028 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb @@ -0,0 +1,101 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Permissions, + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module permissions does not exist|permissions\.)/}, + reason: 'Safari driver currently returns unknown command for BiDi permissions commands'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:permissions) { described_class.new(driver) } + let(:script) { Script.new(driver) } + let(:browsing_context) { BrowsingContext.new(driver) } + + def target(context) + Script::ContextTarget.new(context: context) + end + + def evaluate(expression, context = driver.window_handle, await_promise: false) + script.evaluate(expression: expression, target: target(context), await_promise: await_promise) + end + + def geolocation_permission(context = driver.window_handle) + evaluate( + "navigator.permissions.query({name: 'geolocation'}).then(permission => permission.state)", + context, + await_promise: true + ).result.value + end + + def origin(context = driver.window_handle) + evaluate('window.location.origin', context).result.value + end + + describe '#set_permission' do + it 'sets geolocation permission to granted' do + browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) + + permissions.set_permission( + descriptor: Permissions::PermissionDescriptor.new(name: 'geolocation'), + state: :granted, + origin: origin + ) + + expect(geolocation_permission).to eq('granted') + end + + it 'accepts embedded origin and user context parameters' do + browser = Browser.new(driver) + user_context = browser.create_user_context.user_context + context = browsing_context.create(type: :tab, user_context: user_context).context + + browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) + browsing_context.navigate(context: context, url: url_for('blank.html'), wait: :complete) + permission_origin = origin(driver.window_handle) + original_state = geolocation_permission(driver.window_handle) + + permissions.set_permission( + descriptor: Permissions::PermissionDescriptor.new(name: 'geolocation'), + state: :granted, + origin: permission_origin, + embedded_origin: permission_origin, + user_context: user_context + ) + + expect(geolocation_permission(context)).to eq('granted') + expect(geolocation_permission(driver.window_handle)).to eq(original_state) + ensure + browsing_context.close(context: context) if context + browser.remove_user_context(user_context: user_context) if browser && user_context + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb new file mode 100644 index 0000000000000..ad545f937f076 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -0,0 +1,190 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Script, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:script) { described_class.new(driver) } + + def target(context = driver.window_handle, sandbox: nil) + kwargs = {context: context} + kwargs[:sandbox] = sandbox if sandbox + Script::ContextTarget.new(**kwargs) + end + + def evaluate(expression, **kwargs) + script.evaluate( + expression: expression, + target: kwargs.fetch(:target, target), + await_promise: kwargs.fetch(:await_promise, false), + result_ownership: kwargs.fetch(:result_ownership, WebDriver::BiDi::Serialization::UNSET), + serialization_options: kwargs.fetch(:serialization_options, WebDriver::BiDi::Serialization::UNSET) + ) + end + + describe '#add_preload_script', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /script\.addPreloadScript/}, + reason: 'Safari driver currently returns an error for BiDi preload scripts'} do + it 'runs a preload script in future documents' do + result = script.add_preload_script( + function_declaration: "() => { window.__ruby_bidi_preload = 'installed'; }" + ) + + driver.navigate.to url_for('blank.html') + + expect(result).to be_a(Script::AddPreloadScriptResult) + expect(result.script).to be_a(String) + expect(evaluate('window.__ruby_bidi_preload').result.value).to eq('installed') + ensure + script.remove_preload_script(script: result.script) if result&.script + end + + it 'accepts context and sandbox options' do + result = script.add_preload_script( + function_declaration: '() => { globalThis.__ruby_bidi_sandbox_preload = 7; }', + contexts: [driver.window_handle], + sandbox: 'ruby-preload' + ) + + driver.navigate.to url_for('blank.html') + sandbox_result = evaluate( + 'globalThis.__ruby_bidi_sandbox_preload', + target: target(driver.window_handle, sandbox: 'ruby-preload') + ) + + expect(sandbox_result.result.value).to eq(7) + ensure + script.remove_preload_script(script: result.script) if result&.script + end + end + + describe '#call_function' do + it 'calls a function with local value arguments' do + result = script.call_function( + function_declaration: '(left, right) => left + right', + await_promise: false, + target: target, + arguments: [Script::NumberValue.new(value: 2), Script::NumberValue.new(value: 3)] + ) + + expect(result).to be_a(Script::EvaluateResultSuccess) + expect(result.result).to eq(Script::NumberValue.new(value: 5)) + end + + it 'accepts this, ownership, serialization, and user activation options' do + result = script.call_function( + function_declaration: 'function (suffix) { return this.prefix + suffix; }', + await_promise: true, + target: target, + arguments: [Script::StringValue.new(value: 'BiDi')], + result_ownership: :none, + serialization_options: Script::SerializationOptions.new(max_object_depth: 1), + this: Script::ObjectLocalValue.new(value: [['prefix', Script::StringValue.new(value: 'Ruby ')]]), + user_activation: true + ) + + expect(result).to be_a(Script::EvaluateResultSuccess) + expect(result.result).to eq(Script::StringValue.new(value: 'Ruby BiDi')) + end + end + + describe '#disown' do + it 'disowns a remote handle' do + result = evaluate('({answer: 42})', result_ownership: :root) + handle = result.result.handle + + expect(handle).to be_a(String) + expect(script.disown(handles: [handle], target: target)).to be_empty + end + end + + describe '#evaluate' do + it 'evaluates an expression in the current context' do + result = evaluate('1 + 2') + + expect(result).to be_a(Script::EvaluateResultSuccess) + expect(result.result).to eq(Script::NumberValue.new(value: 3)) + expect(result.realm).to be_a(String) + end + + it 'awaits promises and accepts serialization options' do + result = evaluate( + 'Promise.resolve({name: "ruby", nested: {hidden: true}})', + await_promise: true, + result_ownership: :root, + serialization_options: Script::SerializationOptions.new(max_object_depth: 1) + ) + + expect(result).to be_a(Script::EvaluateResultSuccess) + expect(result.result).to be_a(Script::ObjectRemoteValue) + expect(result.result.handle).to be_a(String) + ensure + if result&.result.respond_to?(:handle) && result.result.handle.is_a?(String) + script.disown(handles: [result.result.handle], target: target) + end + end + end + + describe '#get_realms' do + it 'returns window realms' do + result = script.get_realms + + expect(result.realms).not_to be_empty + expect(result.realms).to all(respond_to(:realm)) + end + + it 'filters realms by context and type' do + result = script.get_realms(context: driver.window_handle, type: :window) + + expect(result.realms).not_to be_empty + expect(result.realms.map(&:context)).to all(eq(driver.window_handle)) + expect(result.realms.map { |realm| realm.type.to_s }.uniq).to eq(['window']) + end + end + + describe '#remove_preload_script', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /script\.(?:addPreloadScript|removePreloadScript)/}, + reason: 'Safari driver currently returns an error for BiDi preload scripts'} do + it 'removes a preload script before future navigations' do + result = script.add_preload_script( + function_declaration: '() => { window.__ruby_bidi_removed_preload = true; }' + ) + + script.remove_preload_script(script: result.script) + driver.navigate.to url_for('blank.html') + + expect(evaluate('typeof window.__ruby_bidi_removed_preload').result.value).to eq('undefined') + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb new file mode 100644 index 0000000000000..eb08e339a285e --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb @@ -0,0 +1,105 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Session, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:session) { described_class.new(driver) } + + describe '#status' do + it 'returns typed session status' do + result = session.status + + expect(result).to be_a(Session::StatusResult) + expect(result.ready).to be(true).or be(false) + expect(result.message).to be_a(String) + end + end + + describe '#subscribe' do + it 'subscribes to an event globally' do + result = session.subscribe(events: ['browsingContext.load']) + + expect(result).to be_a(Session::SubscribeResult) + expect(result.subscription).to be_a(String) + ensure + begin + session.unsubscribe(events: ['browsingContext.load']) + rescue StandardError + nil + end + end + + it 'subscribes to an event for a context' do + result = session.subscribe(events: ['browsingContext.load'], contexts: [driver.window_handle]) + + expect(result.subscription).to be_a(String) + ensure + session.unsubscribe(subscriptions: [result.subscription]) if result&.subscription + end + end + + describe '#unsubscribe' do + it 'unsubscribes by event name' do + session.subscribe(events: ['browsingContext.load']) + + expect(session.unsubscribe(events: ['browsingContext.load'])).to be_empty + end + + it 'unsubscribes by subscription id' do + result = session.subscribe(events: ['browsingContext.load'], contexts: [driver.window_handle]) + + expect(session.unsubscribe(subscriptions: [result.subscription])).to be_empty + end + end + + describe '#new' do + it 'is rejected on an already established WebDriver BiDi session' do + capabilities = Session::CapabilitiesRequest.new( + always_match: Session::CapabilityRequest.new( + accept_insecure_certs: false, + unhandled_prompt_behavior: Session::UserPromptHandler.new(default: :dismiss) + ) + ) + + expect { session.new(capabilities: capabilities) }.to raise_error(Error::WebDriverError) + end + end + + describe '#end_', + pending_if: {browser: :firefox, + exception: {class: Error::UnsupportedOperationError, + message: /Ending a session /}, + reason: 'Firefox returns unsupported operation for session.end on Classic sessions'} do + it 'ends the active BiDi session' do + expect(session.end_).to be_empty + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb new file mode 100644 index 0000000000000..3879c6a070553 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb @@ -0,0 +1,154 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' +require 'uri' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe Storage, + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /(?:Module storage does not exist|storage\.)/}, + reason: 'Safari driver currently returns unknown command for BiDi storage commands'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:storage) { described_class.new(driver) } + + def cookie_domain + URI(driver.current_url).host + end + + def cookie_value(value) + Network::StringValue.new(value: value) + end + + def partial_cookie(name, value, **options) + Storage::PartialCookie.new(name: name, value: cookie_value(value), domain: cookie_domain, **options) + end + + before do + driver.navigate.to url_for('ajaxy_page.html') + driver.manage.delete_all_cookies + end + + describe '#set_cookie' do + it 'sets a cookie that can be read by name' do + result = storage.set_cookie(cookie: partial_cookie('ruby-bidi-cookie', 'test-value')) + + cookies = storage.get_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-cookie') + ).cookies + + expect(result.partition_key).to be_a(Storage::PartitionKey) + expect(cookies.map(&:name)).to include('ruby-bidi-cookie') + expect(cookies.find { |cookie| cookie.name == 'ruby-bidi-cookie' }.value.value).to eq('test-value') + end + + it 'sets a cookie with optional attributes and a context partition' do + partition = Storage::BrowsingContextPartitionDescriptor.new(context: driver.window_handle) + expiry = Time.now.to_i + 3600 + + result = storage.set_cookie( + cookie: partial_cookie( + 'ruby-bidi-partitioned-cookie', + 'partitioned', + path: '/', + http_only: true, + secure: false, + same_site: :lax, + expiry: expiry + ), + partition: partition + ) + + cookies = storage.get_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-partitioned-cookie'), + partition: partition + ).cookies + + expect(result.partition_key).to be_a(Storage::PartitionKey) + expect(cookies.first.value.value).to eq('partitioned') + expect(cookies.first.http_only).to be(true) + expect(cookies.first.same_site).to eq(:lax) + end + end + + describe '#get_cookies' do + it 'returns matching cookies and the partition key' do + storage.set_cookie(cookie: partial_cookie('ruby-bidi-filter-cookie', 'filter-value')) + + result = storage.get_cookies(filter: Storage::CookieFilter.new(domain: cookie_domain)) + + expect(result).to be_a(Storage::GetCookiesResult) + expect(result.partition_key).to be_a(Storage::PartitionKey) + expect(result.cookies.map(&:name)).to include('ruby-bidi-filter-cookie') + end + + it 'returns an empty list when no cookies match the filter' do + result = storage.get_cookies(filter: Storage::CookieFilter.new(name: 'missing-ruby-bidi-cookie')) + + expect(result.cookies).to be_empty + end + end + + describe '#delete_cookies' do + it 'deletes a single matching cookie' do + storage.set_cookie(cookie: partial_cookie('ruby-bidi-delete-me', 'delete')) + storage.set_cookie(cookie: partial_cookie('ruby-bidi-keep-me', 'keep')) + + result = storage.delete_cookies(filter: Storage::CookieFilter.new(name: 'ruby-bidi-delete-me')) + + expect(result.partition_key).to be_a(Storage::PartitionKey) + deleted_cookies = storage.get_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-delete-me') + ).cookies + kept_cookies = storage.get_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-keep-me') + ).cookies + + expect(deleted_cookies).to be_empty + expect(kept_cookies).not_to be_empty + end + + it 'accepts a partition descriptor' do + partition = Storage::BrowsingContextPartitionDescriptor.new(context: driver.window_handle) + storage.set_cookie(cookie: partial_cookie('ruby-bidi-context-cookie', 'context'), partition: partition) + + result = storage.delete_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-context-cookie'), + partition: partition + ) + + expect(result.partition_key).to be_a(Storage::PartitionKey) + expect(storage.get_cookies( + filter: Storage::CookieFilter.new(name: 'ruby-bidi-context-cookie'), + partition: partition + ).cookies).to be_empty + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb new file mode 100644 index 0000000000000..9727f2b8e0387 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb @@ -0,0 +1,106 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe UserAgentClientHints, + pending_if: [{browser: :firefox, + exception: {class: Error::UnknownCommandError, + message: /userAgentClientHints\.setClientHintsOverride/}, + reason: 'Firefox driver currently returns unknown command for userAgentClientHints'}, + {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /userAgentClientHints\.setClientHintsOverride/}, + reason: 'Safari driver currently returns unknown command for userAgentClientHints'}], + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + after { |example| reset_driver!(example: example) } + + let(:user_agent_client_hints) { described_class.new(driver) } + let(:script) { Script.new(driver) } + + def target + Script::ContextTarget.new(context: driver.window_handle) + end + + def evaluate(expression) + script.evaluate(expression: expression, target: target, await_promise: false).result.value + end + + def client_hints(platform: 'RubyOS') + described_class::ClientHintsMetadata.new( + brands: [described_class::BrandVersion.new(brand: 'RubyBrowser', version: '1')], + full_version_list: [described_class::BrandVersion.new(brand: 'RubyBrowser', version: '1.0.0')], + platform: platform, + platform_version: '1.0', + architecture: 'x86', + model: '', + mobile: false, + bitness: '64', + wow64: false, + form_factors: ['Desktop'] + ) + end + + describe '#set_client_hints_override' do + it 'overrides navigator user agent data for the current context' do + user_agent_client_hints.set_client_hints_override( + client_hints: client_hints, + contexts: [driver.window_handle] + ) + + driver.navigate.to url_for('blank.html') + + expect(evaluate('navigator.userAgentData.platform')).to eq('RubyOS') + expect(evaluate('navigator.userAgentData.brands[0].brand')).to eq('RubyBrowser') + ensure + begin + user_agent_client_hints.set_client_hints_override( + client_hints: nil, + contexts: [driver.window_handle] + ) + rescue StandardError + nil + end + end + + it 'accepts user-context filters' do + user_context = Browser.new(driver).create_user_context.user_context + + expect(user_agent_client_hints.set_client_hints_override( + client_hints: client_hints(platform: 'RubyUserContextOS'), + user_contexts: [user_context] + )).to be_empty + ensure + if user_context + user_agent_client_hints.set_client_hints_override(client_hints: nil, + user_contexts: [user_context]) + end + Browser.new(driver).remove_user_context(user_context: user_context) if user_context + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb new file mode 100644 index 0000000000000..0f8f0bbfa9161 --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb @@ -0,0 +1,127 @@ +# 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. + +require_relative '../../spec_helper' +require 'base64' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + describe WebExtension, + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError, + message: /webExtension\./}, + reason: 'Safari driver currently returns unsupported for webExtension commands'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + before { reset_driver!(args: chromium_web_extension_args) if GlobalTestEnv.browser_family == :chromium } + after { |example| reset_driver!(example: example, args: chromium_web_extension_args) } + + let(:web_extension) { described_class.new(driver) } + let(:expected_id) { 'webextensions-selenium-example-v3@example.com' } + + def chromium_web_extension_args + return [] unless GlobalTestEnv.browser_family == :chromium + + %w[--enable-unsafe-extension-debugging --remote-debugging-pipe] + end + + def extension_path(name) + File.expand_path("../../../../../../../common/extensions/#{name}", __dir__) + end + + def archive_extension + return 'webextensions-selenium-example.crx' if GlobalTestEnv.browser_family == :chromium + + 'webextensions-selenium-example.xpi' + end + + def expect_extension_injected + driver.navigate.to url_for('blank.html') + + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq('Content injected by webextensions-selenium-example') + end + + def install_and_assert(extension_data) + result = web_extension.install(extension_data: extension_data) + + expect(result).to be_a(WebExtension::InstallResult) + expect(result.extension).to be_a(String) + expect(result.extension).not_to be_empty + expect(result.extension).to eq(expected_id) if GlobalTestEnv.browser == :firefox + expect_extension_injected + + result + end + + describe '#install' do + it 'installs an extension from a directory path' do + result = install_and_assert( + WebExtension::ExtensionPath.new(path: extension_path('webextensions-selenium-example-signed')) + ) + + web_extension.uninstall(extension: result.extension) + driver.navigate.refresh + expect(driver.find_elements(id: 'webextensions-selenium-example')).to be_empty + end + + it 'installs an extension from an archive path', + pending_if: {browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /Archived and Base64 extensions are not supported/}, + reason: 'Chromium driver currently returns unsupported for archivePath payloads'} do + result = install_and_assert( + WebExtension::ExtensionArchivePath.new(path: extension_path(archive_extension)) + ) + + expect(result.extension).to be_a(String) + web_extension.uninstall(extension: result.extension) + end + + it 'installs an extension from base64 archive data', + pending_if: {browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /Archived and Base64 extensions are not supported/}, + reason: 'Chromium driver currently returns unsupported for base64 payloads'} do + encoded = Base64.strict_encode64(File.binread(extension_path(archive_extension))) + result = install_and_assert(WebExtension::ExtensionBase64Encoded.new(value: encoded)) + + expect(result.extension).to be_a(String) + web_extension.uninstall(extension: result.extension) + end + end + + describe '#uninstall' do + it 'uninstalls an installed extension' do + result = web_extension.install( + extension_data: WebExtension::ExtensionPath.new( + path: extension_path('webextensions-selenium-example-signed') + ) + ) + + expect(web_extension.uninstall(extension: result.extension)).to be_empty + end + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb index 879bb0511ff1e..abb21011ab60f 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb @@ -84,40 +84,26 @@ module Protocol expect(driver.execute_script('return [window.innerWidth, window.innerHeight]')).to eq([800, 600]) end - it 'accepts user prompts without text', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'alert').click - wait_for_alert - browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') + it 'returns a no such alert error when no prompt is open' do + expect { + browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) + }.to raise_error(Error::NoSuchAlertError) end - it 'accepts user prompts with text', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'prompt').click - wait_for_alert - browsing_context.handle_user_prompt(context: driver.window_handle, accept: true, user_text: 'Hello, world!') - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') + it 'accepts user text in the protocol payload' do + expect { + browsing_context.handle_user_prompt( + context: driver.window_handle, + accept: true, + user_text: 'Hello, world!' + ) + }.to raise_error(Error::NoSuchAlertError) end - it 'rejects user prompts', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'alert').click - wait_for_alert - browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') + it 'accepts a reject prompt protocol payload' do + expect { + browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) + }.to raise_error(Error::NoSuchAlertError) end it 'activates a browser context', From 1375d0bbb1b6cd22c313560f6d3cbb18a5c4bc2a Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 14:16:27 -0500 Subject: [PATCH 02/21] [build] run edge-bidi targets in ci-ruby full suite --- .github/workflows/ci-ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-ruby.yml b/.github/workflows/ci-ruby.yml index 30dcc3652cd4b..a231aae9d1ead 100644 --- a/.github/workflows/ci-ruby.yml +++ b/.github/workflows/ci-ruby.yml @@ -91,7 +91,7 @@ jobs: fail-fast: false matrix: os: [windows] - tag_filters: [chrome-beta, firefox-beta, edge-local] + tag_filters: [chrome-beta, firefox-beta, "edge-local,edge-bidi"] include: - os: macos tag_filters: safari-local,safari-preview-bidi From fbe42ecc411f0f6d78d8ad92dc426a53b37189f2 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 17:29:35 -0500 Subject: [PATCH 03/21] [rb] raise BiDi SerializationError on wire deserialization mismatches --- rb/lib/selenium/webdriver/bidi/error.rb | 4 ++++ .../webdriver/bidi/serialization/record.rb | 14 +++++++------- .../selenium/webdriver/bidi/serialization/union.rb | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/rb/lib/selenium/webdriver/bidi/error.rb b/rb/lib/selenium/webdriver/bidi/error.rb index 9331efafd2d44..cb53231f0c3d1 100644 --- a/rb/lib/selenium/webdriver/bidi/error.rb +++ b/rb/lib/selenium/webdriver/bidi/error.rb @@ -23,6 +23,10 @@ module Selenium module WebDriver module Error + # Raised locally when a BiDi wire payload does not match this Selenium's generated + # schema. It is not a protocol error code; the (de)serialization layer raises it directly. + class SerializationError < WebDriverError; end + # Register each BiDi-only code as a WebDriverError subclass; shared codes keep their classic class. BiDi::Protocol::ErrorCode::CLASS_NAMES.each_value do |name| const_set(name, Class.new(WebDriverError)) unless const_defined?(name, false) diff --git a/rb/lib/selenium/webdriver/bidi/serialization/record.rb b/rb/lib/selenium/webdriver/bidi/serialization/record.rb index 679357dab7183..00a198c7b4f55 100644 --- a/rb/lib/selenium/webdriver/bidi/serialization/record.rb +++ b/rb/lib/selenium/webdriver/bidi/serialization/record.rb @@ -85,7 +85,7 @@ def new(**kwargs) # (extensible) or warned and dropped (closed) — strict on shape, lenient on extras. def from_json(json_payload) unless json_payload.is_a?(::Hash) - raise Error::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}" + raise Error::SerializationError, "#{name} expected an object on the wire, got #{json_payload.inspect}" end attributes = fields.to_h do |f| @@ -228,7 +228,7 @@ def wire_value(field, json_payload) # which matters for the required-and-nullable fields the schema flags. def missing_required(field) message = "#{name}##{field.name} is required but was missing from the response" - raise Error::WebDriverError, message if Serialization.strict? + raise Error::SerializationError, message if Serialization.strict? WebDriver.logger.warn(message, id: :bidi_missing_required) UNSET @@ -238,7 +238,7 @@ def read(field, raw) if raw.nil? return raw if field.nullable - raise Error::WebDriverError, "#{name}##{field.name} received null but is not nullable" + raise Error::SerializationError, "#{name}##{field.name} received null but is not nullable" end check_shape(field, raw) return Serialization.to_symbol("#{name}##{field.name}", raw, enum_hash(field)) if field.enum @@ -268,7 +268,7 @@ def check_shape(field, raw) return if field.list == raw.is_a?(::Array) return unless field.list || field.enum || field.ref - raise Error::WebDriverError, + raise Error::SerializationError, "#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}" end @@ -285,7 +285,7 @@ def check_primitive(field, raw) expected = PRIMITIVE_TYPES[field.primitive] return if expected.nil? || expected.any? { |type| raw.is_a?(type) } - raise Error::WebDriverError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}" + raise Error::SerializationError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}" end def enum_hash(field) @@ -314,7 +314,7 @@ def read_list(field, raw, klass) # malformed entry and is rejected outright. def read_map_entry(field, element, klass) unless element.is_a?(::Array) && element.size == 2 - raise Error::WebDriverError, + raise Error::SerializationError, "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}" end @@ -332,7 +332,7 @@ def scalar_value(field, value) expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] } return value if expected.empty? || expected.any? { |type| value.is_a?(type) } - raise Error::WebDriverError, + raise Error::SerializationError, "#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}" end diff --git a/rb/lib/selenium/webdriver/bidi/serialization/union.rb b/rb/lib/selenium/webdriver/bidi/serialization/union.rb index 551ff2addc699..7eb19754f9682 100644 --- a/rb/lib/selenium/webdriver/bidi/serialization/union.rb +++ b/rb/lib/selenium/webdriver/bidi/serialization/union.rb @@ -60,12 +60,12 @@ def from_json(json_payload) unless json_payload.is_a?(::Hash) return json_payload unless @object_only - raise Error::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}" + raise Error::SerializationError, "#{name} expected an object on the wire, got #{json_payload.inspect}" end variant = select(json_payload) unless variant - raise Error::WebDriverError, + raise Error::SerializationError, "#{name} received a variant not in this Selenium's BiDi schema: #{json_payload.inspect}" end Protocol.const_get(variant).from_json(json_payload) From e4949082466553d888c64e58ca345f256fc351ba Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 17:29:43 -0500 Subject: [PATCH 04/21] [rb] guard bidi protocol specs for unsupported browser commands and timeouts --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 34 ++++++++++----- .../webdriver/bidi/protocol/browser_spec.rb | 11 +++-- .../bidi/protocol/browsing_context_spec.rb | 41 +++++++++++++------ .../webdriver/bidi/protocol/emulation_spec.rb | 21 +++++----- .../webdriver/bidi/protocol/input_spec.rb | 14 ++++--- .../webdriver/bidi/protocol/network_spec.rb | 3 +- .../bidi/protocol/permissions_spec.rb | 3 +- .../webdriver/bidi/protocol/script_spec.rb | 21 ++++++---- .../webdriver/bidi/protocol/session_spec.rb | 10 ++++- .../webdriver/bidi/protocol/storage_spec.rb | 5 +-- .../protocol/user_agent_client_hints_spec.rb | 6 +-- .../bidi/protocol/web_extension_spec.rb | 12 ++++-- 12 files changed, 114 insertions(+), 67 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 5a09b94c173c5..9e2f12bafc9b6 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -26,12 +26,10 @@ class BiDi module Protocol describe Bluetooth, pending_if: [{browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /(?:Module bluetooth does not exist|bluetooth\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox returns unknown command for BiDi Bluetooth simulation'}, {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module bluetooth does not exist|bluetooth\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari returns unknown command for BiDi Bluetooth simulation'}], skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after do |example| @@ -283,13 +281,17 @@ def add_descriptor end describe '#handle_request_device_prompt' do - it 'accepts a prompt' do + it 'accepts a prompt', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end - it 'cancels a prompt' do + it 'cancels a prompt', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do events, callback = subscribe('bluetooth.requestDevicePromptUpdated') enable_adapter start_request_device @@ -332,7 +334,9 @@ def add_descriptor end describe '#simulate_advertisement' do - it 'simulates an advertisement scan entry' do + it 'simulates an advertisement scan entry', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do events, callback = subscribe('bluetooth.requestDevicePromptUpdated') enable_adapter start_request_device @@ -350,7 +354,9 @@ def add_descriptor end describe '#simulate_gatt_connection_response' do - it 'simulates a successful GATT connection response' do + it 'simulates a successful GATT connection response', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device connect_selected_device @@ -359,7 +365,9 @@ def add_descriptor end describe '#simulate_gatt_disconnection' do - it 'simulates a GATT disconnection' do + it 'simulates a GATT disconnection', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device connect_selected_device @@ -415,7 +423,9 @@ def add_descriptor end describe '#simulate_characteristic_response' do - it 'simulates characteristic read and write responses' do + it 'simulates characteristic read and write responses', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device add_service add_characteristic @@ -498,7 +508,9 @@ def add_descriptor end describe '#simulate_descriptor_response' do - it 'simulates descriptor read and write responses' do + it 'simulates descriptor read and write responses', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device add_service add_characteristic diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb index 74df9e9da4223..ba0492f9a45e4 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb @@ -36,8 +36,7 @@ def create_user_context context 'with user contexts and client windows', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module browser does not exist|browser\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari returns unknown command for BiDi user contexts/client windows'} do describe '#create_user_context' do it 'returns the created user context id' do @@ -119,7 +118,9 @@ def create_user_context end describe '#set_download_behavior' do - it 'allows downloads into a requested folder' do + it 'allows downloads into a requested folder', + skip_if: {browser: %i[chrome firefox], platform: :windows, + reason: 'Times out waiting for the download to complete'} do Dir.mktmpdir('selenium-bidi-downloads') do |directory| behavior = Browser::DownloadBehavior::Allowed.new(destination_folder: directory) browser.set_download_behavior(download_behavior: behavior) @@ -151,7 +152,9 @@ def create_user_context pending_if: {browser: :firefox, exception: {class: Error::UnsupportedOperationError, message: /Closing the browser in a session /}, - reason: 'Firefox unsupported operation for browser.close on Classic sessions'} do + reason: 'Firefox unsupported operation for browser.close on Classic sessions'}, + skip_if: {browser_family: :safari, + reason: 'Times out: browser.close hangs on Safari'} do it 'closes the browser session' do driver.navigate.to url_for('blank.html') diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index cb6c151d1591c..39fa17177fe40 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -48,7 +48,10 @@ def evaluate(expression, context = driver.window_handle) end end - describe '#capture_screenshot' do + describe '#capture_screenshot', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari does not implement browsingContext.captureScreenshot'} do it 'returns base64 PNG screenshot data' do browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) @@ -106,8 +109,7 @@ def evaluate(expression, context = driver.window_handle) it 'accepts reference, background, and user context parameters', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module browser does not exist|browser\.|browsingContext)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unknown command for BiDi user contexts'} do user_context = Browser.new(driver).create_user_context.user_context @@ -167,7 +169,10 @@ def evaluate(expression, context = driver.window_handle) end end - describe '#locate_nodes' do + describe '#locate_nodes', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari does not implement browsingContext.locateNodes'} do it 'finds nodes by CSS selector' do browsing_context.navigate(context: driver.window_handle, url: url_for('xhtmlTest.html'), wait: :complete) @@ -219,7 +224,10 @@ def evaluate(expression, context = driver.window_handle) end end - describe '#print' do + describe '#print', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari does not implement browsingContext.print'} do it 'returns base64 PDF data' do browsing_context.navigate(context: driver.window_handle, url: url_for('printPage.html'), wait: :complete) @@ -268,9 +276,11 @@ def evaluate(expression, context = driver.window_handle) message: /browsingContext\.setBypassCSP/}, reason: 'Chromium returns unsupported operation for browsingContext.setBypassCSP'}, {browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /browsingContext\.setBypassCSP/}, - reason: 'Firefox returns unknown command for browsingContext.setBypassCSP'}] do + exception: {class: Error::UnknownCommandError}, + reason: 'Firefox returns unknown command for browsingContext.setBypassCSP'}, + {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari does not implement browsingContext.setBypassCSP'}] do it 'sets and clears CSP bypass for a context' do expect(browsing_context.set_bypass_csp(bypass: true, contexts: [driver.window_handle])).to be_empty expect(browsing_context.set_bypass_csp(bypass: nil, contexts: [driver.window_handle])).to be_empty @@ -299,10 +309,13 @@ def evaluate(expression, context = driver.window_handle) end describe '#start_screencast', - pending_if: {browser_family: :chromium, - exception: {class: Error::UnsupportedOperationError, - message: /browsingContext\.startScreencast/}, - reason: 'Chromium returns unsupported operation for browsingContext.startScreencast'} do + pending_if: [{browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError, + message: /browsingContext\.startScreencast/}, + reason: 'Chromium returns unsupported operation for browsingContext.startScreencast'}, + {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari does not implement browsingContext.startScreencast'}] do it 'starts and stops a screencast' do result = browsing_context.start_screencast( context: driver.window_handle, @@ -318,7 +331,9 @@ def evaluate(expression, context = driver.window_handle) end end - describe '#traverse_history' do + describe '#traverse_history', + skip_if: {browser_family: :safari, + reason: 'Times out: browsingContext.traverseHistory hangs on Safari'} do it 'moves backward and forward in the context history' do browsing_context.navigate(context: driver.window_handle, url: url_for('blank.html'), wait: :complete) browsing_context.navigate( diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb index b6fe103dd3b4d..29c6297cffcf0 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb @@ -26,8 +26,7 @@ class BiDi module Protocol describe Emulation, pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module emulation does not exist|emulation\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unknown command for BiDi emulation commands'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } @@ -55,8 +54,7 @@ def create_context message: /emulation\.setForcedColorsModeThemeOverride/}, reason: 'Chromium unsupported operation: emulation.setForcedColorsModeThemeOverride'}, {browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /emulation\.setForcedColorsModeThemeOverride/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox unknown command: emulation.setForcedColorsModeThemeOverride'}] do it 'sets and clears forced-colors theme override' do expect(emulation.set_forced_colors_mode_theme_override( @@ -84,7 +82,11 @@ def create_context )).to be_empty end - it 'sets a geolocation error override' do + it 'sets a geolocation error override', + pending_if: {browser: :firefox, + exception: {class: Error::InvalidArgumentError, + message: /coordinates/}, + reason: 'Firefox does not support the geolocation error override'} do expect(emulation.set_geolocation_override( error: Emulation::GeolocationPositionError.new, contexts: [driver.window_handle] @@ -155,8 +157,7 @@ def create_context describe '#set_scripting_enabled', pending_if: {browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /emulation\.setScriptingEnabled/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox returns unknown command for emulation.setScriptingEnabled'} do it 'disables and restores page scripting' do context = driver.window_handle @@ -187,8 +188,7 @@ def create_context describe '#set_scrollbar_type_override', pending_if: {browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /emulation\.setScrollbarTypeOverride/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox returns unknown command for emulation.setScrollbarTypeOverride'} do it 'sets and clears scrollbar type override' do expect(emulation.set_scrollbar_type_override( @@ -218,8 +218,7 @@ def create_context describe '#set_touch_override', pending_if: {browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /emulation\.setTouchOverride/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox returns unknown command for emulation.setTouchOverride'} do it 'sets and clears touch support' do context = driver.window_handle diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb index 44b1b1f0463dd..a6c90d4f9188e 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb @@ -24,7 +24,11 @@ module Selenium module WebDriver class BiDi module Protocol - describe Input, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + describe Input, + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari script.evaluate result fails deserialization in input setup'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } let(:input) { described_class.new(driver) } @@ -96,10 +100,10 @@ def shared_reference(selector) end describe '#set_files', - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module input does not exist|input\.setFiles)/}, - reason: 'Safari driver currently returns an error for input.setFiles'} do + pending_if: {browser: :firefox, + exception: {class: Error::UnsupportedOperationError, + message: /(?:Unrecognized path|Failed to add file)/}, + reason: 'Firefox rejects the Windows temp file path for input.setFiles'} do it 'sets files on a file input element' do file = create_tempfile driver.navigate.to url_for('upload.html') diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb index 4dbb244af601b..86b1afa9d5579 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb @@ -26,8 +26,7 @@ class BiDi module Protocol describe Network, pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module network does not exist|network\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unknown command for BiDi network commands'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb index c3ca1ce106028..77bf63b8e9d42 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb @@ -26,8 +26,7 @@ class BiDi module Protocol describe Permissions, pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module permissions does not exist|permissions\.)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unknown command for BiDi permissions commands'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index ad545f937f076..0c5f1d89730de 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -47,8 +47,7 @@ def evaluate(expression, **kwargs) describe '#add_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /script\.addPreloadScript/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns an error for BiDi preload scripts'} do it 'runs a preload script in future documents' do result = script.add_preload_script( @@ -83,7 +82,10 @@ def evaluate(expression, **kwargs) end end - describe '#call_function' do + describe '#call_function', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari remote value fails deserialization'} do it 'calls a function with local value arguments' do result = script.call_function( function_declaration: '(left, right) => left + right', @@ -113,7 +115,10 @@ def evaluate(expression, **kwargs) end end - describe '#disown' do + describe '#disown', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari remote value fails deserialization'} do it 'disowns a remote handle' do result = evaluate('({answer: 42})', result_ownership: :root) handle = result.result.handle @@ -123,7 +128,10 @@ def evaluate(expression, **kwargs) end end - describe '#evaluate' do + describe '#evaluate', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari remote value fails deserialization'} do it 'evaluates an expression in the current context' do result = evaluate('1 + 2') @@ -169,8 +177,7 @@ def evaluate(expression, **kwargs) describe '#remove_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /script\.(?:addPreloadScript|removePreloadScript)/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns an error for BiDi preload scripts'} do it 'removes a preload script before future navigations' do result = script.add_preload_script( diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb index eb08e339a285e..1ddad0c5e9aef 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb @@ -39,7 +39,10 @@ module Protocol end end - describe '#subscribe' do + describe '#subscribe', + pending_if: {browser_family: :safari, + exception: {class: Error::InvalidArgumentError}, + reason: 'Safari rejects session.subscribe with invalid argument'} do it 'subscribes to an event globally' do result = session.subscribe(events: ['browsingContext.load']) @@ -62,7 +65,10 @@ module Protocol end end - describe '#unsubscribe' do + describe '#unsubscribe', + pending_if: {browser_family: :safari, + exception: {class: Error::InvalidArgumentError}, + reason: 'Safari rejects session.unsubscribe with invalid argument'} do it 'unsubscribes by event name' do session.subscribe(events: ['browsingContext.load']) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb index 3879c6a070553..f5c07d89e4c15 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb @@ -27,9 +27,8 @@ class BiDi module Protocol describe Storage, pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /(?:Module storage does not exist|storage\.)/}, - reason: 'Safari driver currently returns unknown command for BiDi storage commands'}, + exception: {class: Error::UnknownError}, + reason: 'Safari driver currently returns an internal error for BiDi storage commands'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb index 9727f2b8e0387..a4f02534117a3 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb @@ -26,12 +26,10 @@ class BiDi module Protocol describe UserAgentClientHints, pending_if: [{browser: :firefox, - exception: {class: Error::UnknownCommandError, - message: /userAgentClientHints\.setClientHintsOverride/}, + exception: {class: Error::UnknownCommandError}, reason: 'Firefox driver currently returns unknown command for userAgentClientHints'}, {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /userAgentClientHints\.setClientHintsOverride/}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unknown command for userAgentClientHints'}], skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb index 0f8f0bbfa9161..01263d9751e77 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb @@ -27,12 +27,18 @@ class BiDi module Protocol describe WebExtension, pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError, - message: /webExtension\./}, + exception: {class: Error::UnknownCommandError}, reason: 'Safari driver currently returns unsupported for webExtension commands'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do before { reset_driver!(args: chromium_web_extension_args) if GlobalTestEnv.browser_family == :chromium } - after { |example| reset_driver!(example: example, args: chromium_web_extension_args) } + + after do |example| + if GlobalTestEnv.browser_family == :chromium + reset_driver!(example: example, args: chromium_web_extension_args) + else + reset_driver!(example: example) + end + end let(:web_extension) { described_class.new(driver) } let(:expected_id) { 'webextensions-selenium-example-v3@example.com' } From 6397cfae1a74aac512198167743e8665312eeeab Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 21:51:08 -0500 Subject: [PATCH 05/21] [rb] add edge guards and edge timeout skips for bidi protocol specs --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 14 +++++++------- .../webdriver/bidi/protocol/browser_spec.rb | 2 +- .../bidi/protocol/browsing_context_spec.rb | 15 +++++++++------ .../webdriver/bidi/protocol/emulation_spec.rb | 4 ++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 9e2f12bafc9b6..fa970c4fd1934 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -282,7 +282,7 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device @@ -290,7 +290,7 @@ def add_descriptor end it 'cancels a prompt', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do events, callback = subscribe('bluetooth.requestDevicePromptUpdated') enable_adapter @@ -335,7 +335,7 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do events, callback = subscribe('bluetooth.requestDevicePromptUpdated') enable_adapter @@ -355,7 +355,7 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device @@ -366,7 +366,7 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device connect_selected_device @@ -424,7 +424,7 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device add_service @@ -509,7 +509,7 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do select_device add_service diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb index ba0492f9a45e4..a525bd312efa8 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb @@ -119,7 +119,7 @@ def create_user_context describe '#set_download_behavior' do it 'allows downloads into a requested folder', - skip_if: {browser: %i[chrome firefox], platform: :windows, + skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out waiting for the download to complete'} do Dir.mktmpdir('selenium-bidi-downloads') do |directory| behavior = Browser::DownloadBehavior::Allowed.new(destination_folder: directory) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 39fa17177fe40..f04a71ff41da9 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -271,13 +271,13 @@ def evaluate(expression, context = driver.window_handle) end describe '#set_bypass_csp', - pending_if: [{browser_family: :chromium, + pending_if: [{browser: :chrome, exception: {class: Error::UnsupportedOperationError, message: /browsingContext\.setBypassCSP/}, - reason: 'Chromium returns unsupported operation for browsingContext.setBypassCSP'}, - {browser: :firefox, + reason: 'Chrome returns unsupported operation for browsingContext.setBypassCSP'}, + {browser: %i[edge firefox], exception: {class: Error::UnknownCommandError}, - reason: 'Firefox returns unknown command for browsingContext.setBypassCSP'}, + reason: 'Edge and Firefox return unknown command for browsingContext.setBypassCSP'}, {browser_family: :safari, exception: {class: Error::UnknownCommandError}, reason: 'Safari does not implement browsingContext.setBypassCSP'}] do @@ -309,10 +309,13 @@ def evaluate(expression, context = driver.window_handle) end describe '#start_screencast', - pending_if: [{browser_family: :chromium, + pending_if: [{browser: :chrome, exception: {class: Error::UnsupportedOperationError, message: /browsingContext\.startScreencast/}, - reason: 'Chromium returns unsupported operation for browsingContext.startScreencast'}, + reason: 'Chrome returns unsupported operation for browsingContext.startScreencast'}, + {browser: :edge, + exception: {class: Error::UnknownCommandError}, + reason: 'Edge returns unknown command for browsingContext.startScreencast'}, {browser_family: :safari, exception: {class: Error::UnknownCommandError}, reason: 'Safari does not implement browsingContext.startScreencast'}] do diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb index 29c6297cffcf0..e65a2bb19baac 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb @@ -187,9 +187,9 @@ def create_context end describe '#set_scrollbar_type_override', - pending_if: {browser: :firefox, + pending_if: {browser: %i[edge firefox], exception: {class: Error::UnknownCommandError}, - reason: 'Firefox returns unknown command for emulation.setScrollbarTypeOverride'} do + reason: 'Edge and Firefox return unknown command for setScrollbarTypeOverride'} do it 'sets and clears scrollbar type override' do expect(emulation.set_scrollbar_type_override( scrollbar_type: :classic, From 2c3a80c9dfabf5351497b791bac697be4014dd48 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 4 Aug 2026 21:51:08 -0500 Subject: [PATCH 06/21] [build] run bidi protocol specs in strict serialization mode --- .../selenium/webdriver/bidi/protocol/BUILD.bazel | 1 + rb/spec/tests.bzl | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel index b913d90f09cd1..b5967fb4e181b 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel @@ -14,6 +14,7 @@ filegroup( bidi = True, classic = False, data = ["//common/extensions"], + env = {"SE_BIDI_STRICT": "true"}, tags = ["exclusive-if-local"], deps = [ "//rb/lib/selenium/webdriver:bidi", diff --git a/rb/spec/tests.bzl b/rb/spec/tests.bzl index 33c9c69991aef..47c59dc0ca559 100644 --- a/rb/spec/tests.bzl +++ b/rb/spec/tests.bzl @@ -185,6 +185,7 @@ def rb_integration_test( srcs, deps = [], data = [], + env = {}, browsers = BROWSERS, tags = [], bidi = False, @@ -209,7 +210,7 @@ def rb_integration_test( srcs = srcs, args = ["rb/spec/"], data = BROWSERS[browser]["data"] + data + ["//common/src/web"], - env = BROWSERS[browser]["env"], + env = BROWSERS[browser]["env"] | env, main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + local_tags + ["{}-local".format(browser)] + family_tags, deps = ["//rb/spec/integration/selenium/webdriver:spec_helper"] + BROWSERS[browser]["deps"] + deps, @@ -233,7 +234,7 @@ def rb_integration_test( env = BROWSERS[browser]["env"] | { "WD_BAZEL_JAVA_LOCATION": "$(rootpath //rb/spec:java-location)", "WD_SPEC_DRIVER": "remote", - }, + } | env, main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + ["{}-remote".format(browser)] + family_tags, deps = ["//rb/spec/integration/selenium/webdriver:spec_helper"] + BROWSERS[browser]["deps"] + deps, @@ -248,7 +249,7 @@ def rb_integration_test( srcs = srcs, args = ["rb/spec/"], data = BROWSERS[browser]["data"] + data + ["//common/src/web"], - env = BROWSERS[browser]["env"] | {"WEBDRIVER_BIDI": "true"}, + env = BROWSERS[browser]["env"] | {"WEBDRIVER_BIDI": "true"} | env, main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + ["bidi", "{}-bidi".format(browser)] + family_tags, deps = {d: True for d in ( From a6e40d6202d7f8c3eb172df140d9bc8d58685150 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 08:23:56 -0500 Subject: [PATCH 07/21] [rb] guard Safari bidi protocol specs for strict-mode SerializationError --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 30 +++++++++++++++---- .../webdriver/bidi/protocol/browser_spec.rb | 20 ++++++++++--- .../bidi/protocol/browsing_context_spec.rb | 4 +-- .../webdriver/bidi/protocol/network_spec.rb | 5 +++- .../bidi/protocol/permissions_spec.rb | 5 +++- .../webdriver/bidi/protocol/script_spec.rb | 8 ++--- .../webdriver/bidi/protocol/session_spec.rb | 8 ++--- .../protocol/user_agent_client_hints_spec.rb | 5 +++- 8 files changed, 62 insertions(+), 23 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index fa970c4fd1934..de55984c00484 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -280,7 +280,10 @@ def add_descriptor ) end - describe '#handle_request_device_prompt' do + describe '#handle_request_device_prompt', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'accepts a prompt', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -333,7 +336,10 @@ def add_descriptor end end - describe '#simulate_advertisement' do + describe '#simulate_advertisement', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'simulates an advertisement scan entry', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -353,7 +359,10 @@ def add_descriptor end end - describe '#simulate_gatt_connection_response' do + describe '#simulate_gatt_connection_response', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'simulates a successful GATT connection response', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -364,7 +373,10 @@ def add_descriptor end end - describe '#simulate_gatt_disconnection' do + describe '#simulate_gatt_disconnection', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'simulates a GATT disconnection', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -422,7 +434,10 @@ def add_descriptor end end - describe '#simulate_characteristic_response' do + describe '#simulate_characteristic_response', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'simulates characteristic read and write responses', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -507,7 +522,10 @@ def add_descriptor end end - describe '#simulate_descriptor_response' do + describe '#simulate_descriptor_response', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do it 'simulates descriptor read and write responses', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb index a525bd312efa8..30bce5d7199a0 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb @@ -38,7 +38,10 @@ def create_user_context pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, reason: 'Safari returns unknown command for BiDi user contexts/client windows'} do - describe '#create_user_context' do + describe '#create_user_context', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do it 'returns the created user context id' do user_context = create_user_context @@ -75,7 +78,10 @@ def create_user_context end end - describe '#get_user_contexts' do + describe '#get_user_contexts', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do it 'includes newly created user contexts' do user_contexts = Array.new(2) { create_user_context } all_ids = browser.get_user_contexts.user_contexts.map(&:user_context) @@ -86,7 +92,10 @@ def create_user_context end end - describe '#remove_user_context' do + describe '#remove_user_context', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do it 'removes the requested user context' do user_context = create_user_context @@ -135,7 +144,10 @@ def create_user_context end end - it 'accepts a user-context scoped download behavior' do + it 'accepts a user-context scoped download behavior', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do user_context = create_user_context behavior = Browser::DownloadBehavior::Denied.new diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index f04a71ff41da9..771d545040d92 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -109,8 +109,8 @@ def evaluate(expression, context = driver.window_handle) it 'accepts reference, background, and user context parameters', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unknown command for BiDi user contexts'} do + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do user_context = Browser.new(driver).create_user_context.user_context result = browsing_context.create( diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb index 86b1afa9d5579..bda991c621369 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb @@ -348,7 +348,10 @@ def blocked_event(events, intercept) end end - describe '#set_extra_headers' do + describe '#set_extra_headers', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari network.setExtraHeaders result fails strict deserialization'} do it 'adds extra request headers for a context' do events, callback = subscribe('network.beforeRequestSent') diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb index 77bf63b8e9d42..82465b7871c1d 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb @@ -68,7 +68,10 @@ def origin(context = driver.window_handle) expect(geolocation_permission).to eq('granted') end - it 'accepts embedded origin and user context parameters' do + it 'accepts embedded origin and user context parameters', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do browser = Browser.new(driver) user_context = browser.create_user_context.user_context context = browsing_context.create(type: :tab, user_context: user_context).context diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index 0c5f1d89730de..830b1a205d2b9 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -47,8 +47,8 @@ def evaluate(expression, **kwargs) describe '#add_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns an error for BiDi preload scripts'} do + exception: {class: Error::SerializationError}, + reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'runs a preload script in future documents' do result = script.add_preload_script( function_declaration: "() => { window.__ruby_bidi_preload = 'installed'; }" @@ -177,8 +177,8 @@ def evaluate(expression, **kwargs) describe '#remove_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns an error for BiDi preload scripts'} do + exception: {class: Error::SerializationError}, + reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'removes a preload script before future navigations' do result = script.add_preload_script( function_declaration: '() => { window.__ruby_bidi_removed_preload = true; }' diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb index 1ddad0c5e9aef..d2e7f965997a1 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb @@ -41,8 +41,8 @@ module Protocol describe '#subscribe', pending_if: {browser_family: :safari, - exception: {class: Error::InvalidArgumentError}, - reason: 'Safari rejects session.subscribe with invalid argument'} do + exception: {class: Error::SerializationError}, + reason: 'Safari session.subscribe result fails strict deserialization'} do it 'subscribes to an event globally' do result = session.subscribe(events: ['browsingContext.load']) @@ -67,8 +67,8 @@ module Protocol describe '#unsubscribe', pending_if: {browser_family: :safari, - exception: {class: Error::InvalidArgumentError}, - reason: 'Safari rejects session.unsubscribe with invalid argument'} do + exception: {class: Error::SerializationError}, + reason: 'Safari session.unsubscribe result fails strict deserialization'} do it 'unsubscribes by event name' do session.subscribe(events: ['browsingContext.load']) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb index a4f02534117a3..6272f112f5f2f 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb @@ -82,7 +82,10 @@ def client_hints(platform: 'RubyOS') end end - it 'accepts user-context filters' do + it 'accepts user-context filters', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari create_user_context result fails strict deserialization'} do user_context = Browser.new(driver).create_user_context.user_context expect(user_agent_client_hints.set_client_hints_override( From 187b2c4294a9d81b54e291a792660c1f98507ad6 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 19:18:46 -0500 Subject: [PATCH 08/21] [rb] type inbound BiDi enum errors as SerializationError and relocate handle_user_prompt specs --- .../selenium/webdriver/bidi/serialization.rb | 2 +- .../bidi/protocol/browsing_context_spec.rb | 46 +++++++++++++++---- .../bidi/protocol_browsing_context_spec.rb | 22 --------- .../webdriver/bidi/serialization_spec.rb | 36 +++++++-------- 4 files changed, 57 insertions(+), 49 deletions(-) diff --git a/rb/lib/selenium/webdriver/bidi/serialization.rb b/rb/lib/selenium/webdriver/bidi/serialization.rb index 9e3a05ab3ea65..0525550486a3b 100644 --- a/rb/lib/selenium/webdriver/bidi/serialization.rb +++ b/rb/lib/selenium/webdriver/bidi/serialization.rb @@ -79,7 +79,7 @@ def self.to_symbol(name, value, enum) return value if value.nil? return value.map { |element| to_symbol(name, element, enum) } if value.is_a?(::Array) - enum.key(value) || raise(Error::WebDriverError, "#{name} received an unknown value: #{value.inspect}") + enum.key(value) || raise(Error::SerializationError, "#{name} received an unknown value: #{value.inspect}") end end end # BiDi diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 771d545040d92..06334bde8992e 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -158,14 +158,44 @@ def evaluate(expression, context = driver.window_handle) }.to raise_error(Error::NoSuchAlertError) end - it 'accepts user text in the protocol payload' do - expect { - browsing_context.handle_user_prompt( - context: driver.window_handle, - accept: true, - user_text: 'Ruby BiDi' - ) - }.to raise_error(Error::NoSuchAlertError) + it 'accepts user prompts without text', + pending_if: {browser_family: :chromium, + reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'alert').click + wait_for_alert + browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') + end + + it 'accepts user prompts with text', + pending_if: {browser_family: :chromium, + reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'prompt').click + wait_for_alert + browsing_context.handle_user_prompt( + context: driver.window_handle, + accept: true, + user_text: 'Hello, world!' + ) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') + end + + it 'rejects user prompts', + pending_if: {browser_family: :chromium, + reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'alert').click + wait_for_alert + browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') end end diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb index abb21011ab60f..c36980929f752 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol_browsing_context_spec.rb @@ -84,28 +84,6 @@ module Protocol expect(driver.execute_script('return [window.innerWidth, window.innerHeight]')).to eq([800, 600]) end - it 'returns a no such alert error when no prompt is open' do - expect { - browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) - }.to raise_error(Error::NoSuchAlertError) - end - - it 'accepts user text in the protocol payload' do - expect { - browsing_context.handle_user_prompt( - context: driver.window_handle, - accept: true, - user_text: 'Hello, world!' - ) - }.to raise_error(Error::NoSuchAlertError) - end - - it 'accepts a reject prompt protocol payload' do - expect { - browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) - }.to raise_error(Error::NoSuchAlertError) - end - it 'activates a browser context', pending_if: {browser_family: :safari, reason: 'Safari does not focus the activated context'} do window = driver.window_handle diff --git a/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb b/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb index 2ad1d196182f2..ac18b04afb8f3 100644 --- a/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb +++ b/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb @@ -79,12 +79,12 @@ def valid_cookie_attrs payload = {'type' => 'futuristic', 'value' => 'x'} expect { BrowsingContext::Locator.from_json(payload) } - .to raise_error(Error::WebDriverError, /variant not in this Selenium's BiDi schema/) + .to raise_error(Error::SerializationError, /variant not in this Selenium's BiDi schema/) end it 'raises when an object-only union receives a bare scalar (no arm can match)' do expect { Script::RemoteValue.from_json('not-an-object') } - .to raise_error(Error::WebDriverError, /RemoteValue expected an object/) + .to raise_error(Error::SerializationError, /RemoteValue expected an object/) end it 'passes a bare scalar through a union that has a scalar arm (input.Origin)' do @@ -162,7 +162,7 @@ def valid_cookie_attrs wire = {'type' => 'object', 'value' => [[42, {'type' => 'number', 'value' => 2}]]} expect { Script::ObjectRemoteValue.from_json(wire) } - .to raise_error(Error::WebDriverError, /value expected string, got 42/) + .to raise_error(Error::SerializationError, /value expected string, got 42/) end # Scalar tolerance is the key's alone: the value is the object-only RemoteValue union, @@ -171,7 +171,7 @@ def valid_cookie_attrs wire = {'type' => 'object', 'value' => [['k', 'bare string, not an object']]} expect { Script::ObjectRemoteValue.from_json(wire) } - .to raise_error(Error::WebDriverError, /expected an object on the wire/) + .to raise_error(Error::SerializationError, /expected an object on the wire/) end # A map is `[key, value]` pairs; a malformed entry that is not a 2-item pair is a wire @@ -180,7 +180,7 @@ def valid_cookie_attrs wire = {'type' => 'object', 'value' => [['orphan-key']]} expect { Script::ObjectRemoteValue.from_json(wire) } - .to raise_error(Error::WebDriverError, /expected a \[key, value\] pair/) + .to raise_error(Error::SerializationError, /expected a \[key, value\] pair/) end end @@ -394,7 +394,7 @@ def moz_install(**kwargs) it 'raises on an inbound enum value outside our schema' do expect { Log::ConsoleLogEntry.from_json('type' => 'console', 'level' => 'futureLevel') } - .to raise_error(Error::WebDriverError, /level received an unknown value.*futureLevel/) + .to raise_error(Error::SerializationError, /level received an unknown value.*futureLevel/) end end @@ -545,7 +545,7 @@ def moz_install(**kwargs) it 'raises on an unrecognized inbound token' do expect { Bluetooth::SimulateAdapterParameters.from_json('context' => 'c', 'state' => 'powered-sideways') } - .to raise_error(Error::WebDriverError, /state received an unknown value.*powered-sideways/) + .to raise_error(Error::SerializationError, /state received an unknown value.*powered-sideways/) end it 'coerces each element of a list-valued enum' do @@ -566,7 +566,7 @@ def moz_install(**kwargs) expect(klass.new(scrollbar_type: nil).as_json).to eq('scrollbarType' => nil) expect { klass.new(scrollbar_type: :banana) }.to raise_error(ArgumentError, /must be one of/) expect { klass.from_json('scrollbarType' => 'banana') } - .to raise_error(Error::WebDriverError, /received an unknown value/) + .to raise_error(Error::SerializationError, /received an unknown value/) end end @@ -577,42 +577,42 @@ def moz_install(**kwargs) it 'raises when a non-nullable field arrives as explicit null' do expect { Network::Cookie.from_json(cookie_wire.merge('name' => nil)) } - .to raise_error(Error::WebDriverError, /Cookie#name received null but is not nullable/) + .to raise_error(Error::SerializationError, /Cookie#name received null but is not nullable/) end it 'raises when a list-typed field arrives as a scalar' do expect { Network::AddInterceptParameters.from_json('phases' => 'beforeRequestSent') } - .to raise_error(Error::WebDriverError, /phases expected a list/) + .to raise_error(Error::SerializationError, /phases expected a list/) end it 'raises when a scalar-typed field arrives as a list' do expect { Network::Cookie.from_json(cookie_wire.merge('sameSite' => %w[none])) } - .to raise_error(Error::WebDriverError, /same_site expected a single value/) + .to raise_error(Error::SerializationError, /same_site expected a single value/) end it 'raises when an object-typed record arrives as a scalar' do expect { Network::AuthCredentials.from_json('not-an-object') } - .to raise_error(Error::WebDriverError, /AuthCredentials expected an object/) + .to raise_error(Error::SerializationError, /AuthCredentials expected an object/) end it 'raises when a string-typed field arrives as a number' do expect { Network::Cookie.from_json(cookie_wire.merge('name' => 123)) } - .to raise_error(Error::WebDriverError, /name expected string/) + .to raise_error(Error::SerializationError, /name expected string/) end it 'raises when a boolean-typed field arrives as a string' do expect { Network::Cookie.from_json(cookie_wire.merge('secure' => 'yes')) } - .to raise_error(Error::WebDriverError, /secure expected boolean/) + .to raise_error(Error::SerializationError, /secure expected boolean/) end it 'raises when an integer-typed field arrives as a string' do expect { Bluetooth::BluetoothManufacturerData.from_json('key' => 'nope', 'data' => 'x') } - .to raise_error(Error::WebDriverError, /key expected integer/) + .to raise_error(Error::SerializationError, /key expected integer/) end it 'raises when an integer-typed field arrives as a non-integer float' do expect { Bluetooth::BluetoothManufacturerData.from_json('key' => 1.5, 'data' => 'x') } - .to raise_error(Error::WebDriverError, /key expected integer/) + .to raise_error(Error::SerializationError, /key expected integer/) end it 'accepts an integer for an integer-typed field' do @@ -625,7 +625,7 @@ def moz_install(**kwargs) # its leaf primitive, so a wrong-typed value is rejected instead of passing opaque. it 'raises when an alias-typed integer field (js-uint) arrives as a string' do expect { Network::Cookie.from_json(cookie_wire.merge('size' => 'big')) } - .to raise_error(Error::WebDriverError, /size expected integer/) + .to raise_error(Error::SerializationError, /size expected integer/) end end @@ -646,7 +646,7 @@ def moz_install(**kwargs) allow(ENV).to receive(:fetch).with('SE_BIDI_STRICT', '').and_return('true') expect { Bluetooth::RequestDeviceInfo.from_json('id' => 'dev-1') } - .to raise_error(Error::WebDriverError, /RequestDeviceInfo#name is required but was missing/) + .to raise_error(Error::SerializationError, /RequestDeviceInfo#name is required but was missing/) end end end From 4f5d62be810a56b65cf1becd30396b45e69955cf Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 5 Aug 2026 22:06:50 -0500 Subject: [PATCH 09/21] [rb] skip unsupported BiDi modules on Safari with per-module support probes --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 41 +++----- .../webdriver/bidi/protocol/browser_spec.rb | 34 ++----- .../bidi/protocol/browsing_context_spec.rb | 4 +- .../webdriver/bidi/protocol/emulation_spec.rb | 5 +- .../webdriver/bidi/protocol/network_spec.rb | 56 ++++++++--- .../bidi/protocol/permissions_spec.rb | 10 +- .../protocol/safari_support_probe_spec.rb | 95 +++++++++++++++++++ .../webdriver/bidi/protocol/script_spec.rb | 10 +- .../webdriver/bidi/protocol/storage_spec.rb | 5 +- .../protocol/user_agent_client_hints_spec.rb | 16 ++-- .../bidi/protocol/web_extension_spec.rb | 5 +- 11 files changed, 180 insertions(+), 101 deletions(-) create mode 100644 rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index de55984c00484..5e24c455153e5 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -25,12 +25,11 @@ module WebDriver class BiDi module Protocol describe Bluetooth, - pending_if: [{browser: :firefox, - exception: {class: Error::UnknownCommandError}, - reason: 'Firefox returns unknown command for BiDi Bluetooth simulation'}, - {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari returns unknown command for BiDi Bluetooth simulation'}], + pending_if: {browser: :firefox, + exception: {class: Error::UnknownCommandError}, + reason: 'Firefox returns unknown command for BiDi Bluetooth simulation'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after do |example| next if example.metadata[:skip] || example.skip @@ -280,10 +279,7 @@ def add_descriptor ) end - describe '#handle_request_device_prompt', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#handle_request_device_prompt' do it 'accepts a prompt', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -336,10 +332,7 @@ def add_descriptor end end - describe '#simulate_advertisement', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -359,10 +352,7 @@ def add_descriptor end end - describe '#simulate_gatt_connection_response', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -373,10 +363,7 @@ def add_descriptor end end - describe '#simulate_gatt_disconnection', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -434,10 +421,7 @@ def add_descriptor end end - describe '#simulate_characteristic_response', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do @@ -522,10 +506,7 @@ def add_descriptor end end - describe '#simulate_descriptor_response', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari subscribe/evaluate setup fails strict deserialization'} do + describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', skip_if: {browser: %i[chrome edge firefox], platform: :windows, reason: 'Times out: no in-flight prompt/operation to respond to'} do diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb index 30bce5d7199a0..3fe71856f3d92 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browser_spec.rb @@ -25,7 +25,10 @@ module Selenium module WebDriver class BiDi module Protocol - describe Browser, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + describe Browser, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, + skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } let(:browser) { described_class.new(driver) } @@ -34,14 +37,8 @@ def create_user_context browser.create_user_context.user_context end - context 'with user contexts and client windows', - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari returns unknown command for BiDi user contexts/client windows'} do - describe '#create_user_context', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + context 'with user contexts and client windows' do + describe '#create_user_context' do it 'returns the created user context id' do user_context = create_user_context @@ -78,10 +75,7 @@ def create_user_context end end - describe '#get_user_contexts', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + describe '#get_user_contexts' do it 'includes newly created user contexts' do user_contexts = Array.new(2) { create_user_context } all_ids = browser.get_user_contexts.user_contexts.map(&:user_context) @@ -92,10 +86,7 @@ def create_user_context end end - describe '#remove_user_context', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + describe '#remove_user_context' do it 'removes the requested user context' do user_context = create_user_context @@ -144,10 +135,7 @@ def create_user_context end end - it 'accepts a user-context scoped download behavior', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + it 'accepts a user-context scoped download behavior' do user_context = create_user_context behavior = Browser::DownloadBehavior::Denied.new @@ -164,9 +152,7 @@ def create_user_context pending_if: {browser: :firefox, exception: {class: Error::UnsupportedOperationError, message: /Closing the browser in a session /}, - reason: 'Firefox unsupported operation for browser.close on Classic sessions'}, - skip_if: {browser_family: :safari, - reason: 'Times out: browser.close hangs on Safari'} do + reason: 'Firefox unsupported operation for browser.close on Classic sessions'} do it 'closes the browser session' do driver.navigate.to url_for('blank.html') diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 06334bde8992e..73f6f7deb1c8e 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -318,7 +318,9 @@ def evaluate(expression, context = driver.window_handle) end describe '#set_viewport' do - it 'sets the viewport size and device pixel ratio' do + it 'sets the viewport size and device pixel ratio', + pending_if: {browser_family: :safari, + reason: 'Safari accepts browsingContext.setViewport but does not resize the window'} do browsing_context.set_viewport( context: driver.window_handle, viewport: BrowsingContext::Viewport.new(width: 800, height: 600), diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb index e65a2bb19baac..aba422a254dc3 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/emulation_spec.rb @@ -25,9 +25,8 @@ module WebDriver class BiDi module Protocol describe Emulation, - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unknown command for BiDi emulation commands'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb index bda991c621369..310394ab35148 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb @@ -24,11 +24,7 @@ module Selenium module WebDriver class BiDi module Protocol - describe Network, - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unknown command for BiDi network commands'}, - skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + describe Network, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } let(:network) { described_class.new(driver) } @@ -62,7 +58,10 @@ def blocked_event(events, intercept) end end - describe '#add_data_collector' do + describe '#add_data_collector', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.add_data_collector'} do it 'returns a collector id' do result = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) @@ -86,7 +85,10 @@ def blocked_event(events, intercept) end end - describe '#add_intercept' do + describe '#add_intercept', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.add_intercept'} do it 'returns an intercept id' do result = network.add_intercept(phases: [:before_request_sent]) @@ -109,7 +111,10 @@ def blocked_event(events, intercept) end end - describe '#continue_request' do + describe '#continue_request', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.continue_request'} do it 'continues an intercepted request' do intercept = network.add_intercept(phases: [:before_request_sent]) events, callback = subscribe('network.beforeRequestSent') @@ -164,7 +169,10 @@ def blocked_event(events, intercept) end end - describe '#continue_response' do + describe '#continue_response', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.continue_response'} do it 'continues an intercepted response' do intercept = network.add_intercept(phases: [:response_started]) events, callback = subscribe('network.responseStarted') @@ -212,7 +220,10 @@ def blocked_event(events, intercept) end end - describe '#continue_with_auth' do + describe '#continue_with_auth', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.continue_with_auth'} do it 'provides credentials for an auth challenge' do username, password = SpecSupport::RackServer::TestApp::BASIC_AUTH_CREDENTIALS intercept = network.add_intercept(phases: [:auth_required]) @@ -239,7 +250,10 @@ def blocked_event(events, intercept) end end - describe '#fail_request' do + describe '#fail_request', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.fail_request'} do it 'fails an intercepted request' do intercept = network.add_intercept( phases: [:before_request_sent], @@ -264,7 +278,10 @@ def blocked_event(events, intercept) end end - describe '#get_data' do + describe '#get_data', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.get_data'} do it 'gets and disowns collected response data' do collector = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) events, callback = subscribe('network.responseCompleted') @@ -294,7 +311,10 @@ def blocked_event(events, intercept) end end - describe '#provide_response' do + describe '#provide_response', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.provide_response'} do it 'provides a complete response body' do intercept = network.add_intercept( phases: [:before_request_sent], @@ -325,7 +345,10 @@ def blocked_event(events, intercept) end end - describe '#remove_data_collector' do + describe '#remove_data_collector', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.remove_data_collector'} do it 'removes a collector' do collector = network.add_data_collector(data_types: [:response], max_encoded_data_size: 200_000_000) @@ -333,7 +356,10 @@ def blocked_event(events, intercept) end end - describe '#remove_intercept' do + describe '#remove_intercept', + pending_if: {browser_family: :safari, + exception: {class: Error::UnknownCommandError}, + reason: 'Safari returns unknown command for network.remove_intercept'} do it 'removes an intercept' do intercept = network.add_intercept(phases: [:before_request_sent]) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb index 82465b7871c1d..b5a24fef8882b 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/permissions_spec.rb @@ -25,9 +25,8 @@ module WebDriver class BiDi module Protocol describe Permissions, - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unknown command for BiDi permissions commands'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } @@ -68,10 +67,7 @@ def origin(context = driver.window_handle) expect(geolocation_permission).to eq('granted') end - it 'accepts embedded origin and user context parameters', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + it 'accepts embedded origin and user context parameters' do browser = Browser.new(driver) user_context = browser.create_user_context.user_context context = browsing_context.create(type: :tab, user_context: user_context).context diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb new file mode 100644 index 0000000000000..4898e6b7157ea --- /dev/null +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb @@ -0,0 +1,95 @@ +# 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. + +require_relative '../../spec_helper' +require 'selenium/webdriver/bidi/protocol' + +module Selenium + module WebDriver + class BiDi + module Protocol + # One canary per BiDi module that Safari does not yet implement. The module's own spec is + # skipped on Safari (skip_if) so its dozens of unsupported examples don't burn Safari's slow, + # serial runner. Each probe below runs only on Safari, staying pending while the command + # raises. When Safari implements the module the command succeeds, the expectation runs, and the + # probe passes, raising PendingExampleFixedError so we drop skip_if(:safari) in the named spec. + describe 'Safari BiDi support probes', + skip_unless: {bidi: true, browser_family: :safari, reason: 'probes only run on Safari'} do + after { |example| reset_driver!(example: example) } + + it 'bluetooth is unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock bluetooth_spec.rb'} do + result = Bluetooth.new(driver).simulate_adapter( + context: driver.window_handle, state: :powered_on, le_supported: true + ) + expect(result).to be_empty + end + + it 'browser client windows are unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock browser_spec.rb'} do + expect(Browser.new(driver).get_client_windows.client_windows).to be_an(Array) + end + + it 'emulation is unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock emulation_spec.rb'} do + result = Emulation.new(driver).set_forced_colors_mode_theme_override( + theme: nil, contexts: [driver.window_handle] + ) + expect(result).to be_empty + end + + it 'permissions is unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock permissions_spec.rb'} do + result = Permissions.new(driver).set_permission( + descriptor: Permissions::PermissionDescriptor.new(name: 'geolocation'), + state: :granted, + origin: url_for('blank.html') + ) + expect(result).to be_empty + end + + it 'storage is unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownError}, + reason: 'when green, unblock storage_spec.rb'} do + expect(Storage.new(driver).get_cookies.cookies).to be_an(Array) + end + + it 'user agent client hints are unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock user_agent_client_hints_spec.rb'} do + result = UserAgentClientHints.new(driver).set_client_hints_override( + client_hints: nil, contexts: [driver.window_handle] + ) + expect(result).to be_empty + end + + it 'web extensions are unimplemented on Safari', + pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, + reason: 'when green, unblock web_extension_spec.rb'} do + expect(WebExtension.new(driver).uninstall(extension: 'ruby-bidi-probe')).to be_empty + end + end + end # Protocol + end # BiDi + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index 830b1a205d2b9..1a3cccd16b5ba 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -128,10 +128,7 @@ def evaluate(expression, **kwargs) end end - describe '#evaluate', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari remote value fails deserialization'} do + describe '#evaluate' do it 'evaluates an expression in the current context' do result = evaluate('1 + 2') @@ -140,7 +137,10 @@ def evaluate(expression, **kwargs) expect(result.realm).to be_a(String) end - it 'awaits promises and accepts serialization options' do + it 'awaits promises and accepts serialization options', + pending_if: {browser_family: :safari, + exception: {class: Error::SerializationError}, + reason: 'Safari remote value fails deserialization'} do result = evaluate( 'Promise.resolve({name: "ruby", nested: {hidden: true}})', await_promise: true, diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb index f5c07d89e4c15..6c2a5766ad67c 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/storage_spec.rb @@ -26,9 +26,8 @@ module WebDriver class BiDi module Protocol describe Storage, - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownError}, - reason: 'Safari driver currently returns an internal error for BiDi storage commands'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb index 6272f112f5f2f..e799c93fd1f89 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/user_agent_client_hints_spec.rb @@ -25,12 +25,11 @@ module WebDriver class BiDi module Protocol describe UserAgentClientHints, - pending_if: [{browser: :firefox, - exception: {class: Error::UnknownCommandError}, - reason: 'Firefox driver currently returns unknown command for userAgentClientHints'}, - {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unknown command for userAgentClientHints'}], + pending_if: {browser: :firefox, + exception: {class: Error::UnknownCommandError}, + reason: 'Firefox driver currently returns unknown command for userAgentClientHints'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } @@ -82,10 +81,7 @@ def client_hints(platform: 'RubyOS') end end - it 'accepts user-context filters', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari create_user_context result fails strict deserialization'} do + it 'accepts user-context filters' do user_context = Browser.new(driver).create_user_context.user_context expect(user_agent_client_hints.set_client_hints_override( diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb index 01263d9751e77..2ead067d4cbd4 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/web_extension_spec.rb @@ -26,9 +26,8 @@ module WebDriver class BiDi module Protocol describe WebExtension, - pending_if: {browser_family: :safari, - exception: {class: Error::UnknownCommandError}, - reason: 'Safari driver currently returns unsupported for webExtension commands'}, + skip_if: {browser_family: :safari, + reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do before { reset_driver!(args: chromium_web_extension_args) if GlobalTestEnv.browser_family == :chromium } From 8094039139cce313bf688e15232c5d7b62ab175f Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 6 Aug 2026 11:22:33 -0500 Subject: [PATCH 10/21] [rb] drop exception class from volatile Safari BiDi pending guards --- .../bidi/protocol/browsing_context_spec.rb | 1 - .../webdriver/bidi/protocol/input_spec.rb | 1 - .../webdriver/bidi/protocol/network_spec.rb | 1 - .../webdriver/bidi/protocol/script_spec.rb | 17 +++++++---------- .../webdriver/bidi/protocol/session_spec.rb | 2 -- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 73f6f7deb1c8e..35d5bfa0b84d1 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -109,7 +109,6 @@ def evaluate(expression, context = driver.window_handle) it 'accepts reference, background, and user context parameters', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari create_user_context result fails strict deserialization'} do user_context = Browser.new(driver).create_user_context.user_context diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb index a6c90d4f9188e..c935c5c1703e8 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb @@ -26,7 +26,6 @@ class BiDi module Protocol describe Input, pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari script.evaluate result fails deserialization in input setup'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do after { |example| reset_driver!(example: example) } diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb index 310394ab35148..04695e714bac0 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/network_spec.rb @@ -376,7 +376,6 @@ def blocked_event(events, intercept) describe '#set_extra_headers', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari network.setExtraHeaders result fails strict deserialization'} do it 'adds extra request headers for a context' do events, callback = subscribe('network.beforeRequestSent') diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index 1a3cccd16b5ba..314a821435b1b 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -47,7 +47,6 @@ def evaluate(expression, **kwargs) describe '#add_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'runs a preload script in future documents' do result = script.add_preload_script( @@ -82,11 +81,10 @@ def evaluate(expression, **kwargs) end end - describe '#call_function', - pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, - reason: 'Safari remote value fails deserialization'} do - it 'calls a function with local value arguments' do + describe '#call_function' do + it 'calls a function with local value arguments', + pending_if: {browser_family: :safari, + reason: 'Safari remote value fails deserialization'} do result = script.call_function( function_declaration: '(left, right) => left + right', await_promise: false, @@ -98,7 +96,9 @@ def evaluate(expression, **kwargs) expect(result.result).to eq(Script::NumberValue.new(value: 5)) end - it 'accepts this, ownership, serialization, and user activation options' do + it 'accepts this, ownership, serialization, and user activation options', + pending_if: {browser_family: :safari, + reason: 'Safari call_function returns an unexpected result for this-object binding'} do result = script.call_function( function_declaration: 'function (suffix) { return this.prefix + suffix; }', await_promise: true, @@ -117,7 +117,6 @@ def evaluate(expression, **kwargs) describe '#disown', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari remote value fails deserialization'} do it 'disowns a remote handle' do result = evaluate('({answer: 42})', result_ownership: :root) @@ -139,7 +138,6 @@ def evaluate(expression, **kwargs) it 'awaits promises and accepts serialization options', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari remote value fails deserialization'} do result = evaluate( 'Promise.resolve({name: "ruby", nested: {hidden: true}})', @@ -177,7 +175,6 @@ def evaluate(expression, **kwargs) describe '#remove_preload_script', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'removes a preload script before future navigations' do result = script.add_preload_script( diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb index d2e7f965997a1..5273d93cb064c 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/session_spec.rb @@ -41,7 +41,6 @@ module Protocol describe '#subscribe', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari session.subscribe result fails strict deserialization'} do it 'subscribes to an event globally' do result = session.subscribe(events: ['browsingContext.load']) @@ -67,7 +66,6 @@ module Protocol describe '#unsubscribe', pending_if: {browser_family: :safari, - exception: {class: Error::SerializationError}, reason: 'Safari session.unsubscribe result fails strict deserialization'} do it 'unsubscribes by event name' do session.subscribe(events: ['browsingContext.load']) From 666b9ff4c2f5d48ce9cbf49a0ddb033692271339 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sat, 8 Aug 2026 10:26:06 -0500 Subject: [PATCH 11/21] [rb] scope BiDi protocol spec guards by browser/OS and mirror the real Safari permissions probe --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 43 +++++++++++-------- .../bidi/protocol/browsing_context_spec.rb | 6 ++- .../webdriver/bidi/protocol/input_spec.rb | 2 +- .../protocol/safari_support_probe_spec.rb | 10 ++++- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 5e24c455153e5..686e6fc337e9c 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -126,8 +126,13 @@ def evaluate_value(expression, await_promise: false, user_activation: WebDriver: target: target, await_promise: await_promise, user_activation: user_activation - ).result - result.respond_to?(:value) ? result.value : nil + ) + if result.is_a?(Script::EvaluateResultException) + raise Error::WebDriverError, "script.evaluate raised: #{result.exception_details.text}" + end + + value = result.result + value.respond_to?(:value) ? value.value : nil end def start_request_device @@ -160,8 +165,8 @@ def advertised_device(prompt) end def select_device - events, callback = subscribe('bluetooth.requestDevicePromptUpdated') enable_adapter + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device expect(bluetooth.simulate_advertisement( @@ -281,18 +286,18 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do - events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do enable_adapter + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device expect(bluetooth.simulate_advertisement( @@ -334,10 +339,10 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do - events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do enable_adapter + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device expect(bluetooth.simulate_advertisement( @@ -354,8 +359,8 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do select_device connect_selected_device @@ -365,8 +370,8 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do select_device connect_selected_device @@ -423,8 +428,8 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do select_device add_service add_characteristic @@ -508,8 +513,8 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser: %i[chrome edge firefox], platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to'} do + skip_if: {browser_family: :chromium, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do select_device add_service add_characteristic diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 35d5bfa0b84d1..a9d639acd0007 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -318,8 +318,10 @@ def evaluate(expression, context = driver.window_handle) describe '#set_viewport' do it 'sets the viewport size and device pixel ratio', - pending_if: {browser_family: :safari, - reason: 'Safari accepts browsingContext.setViewport but does not resize the window'} do + pending_if: [{browser_family: :safari, + reason: 'Safari accepts browsingContext.setViewport but does not resize the window'}, + {browser: :firefox, platform: :linux, + reason: 'Firefox accepts setViewport but does not resize the window on Linux'}] do browsing_context.set_viewport( context: driver.window_handle, viewport: BrowsingContext::Viewport.new(width: 800, height: 600), diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb index c935c5c1703e8..08c38cb3ce27f 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/input_spec.rb @@ -99,7 +99,7 @@ def shared_reference(selector) end describe '#set_files', - pending_if: {browser: :firefox, + pending_if: {browser: :firefox, platform: :windows, exception: {class: Error::UnsupportedOperationError, message: /(?:Unrecognized path|Failed to add file)/}, reason: 'Firefox rejects the Windows temp file path for input.setFiles'} do diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb index 4898e6b7157ea..9f02e94b96a33 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/safari_support_probe_spec.rb @@ -60,10 +60,18 @@ module Protocol it 'permissions is unimplemented on Safari', pending_if: {browser_family: :safari, exception: {class: Error::UnknownCommandError}, reason: 'when green, unblock permissions_spec.rb'} do + context = driver.window_handle + BrowsingContext.new(driver).navigate(context: context, url: url_for('blank.html'), wait: :complete) + origin = Script.new(driver).evaluate( + expression: 'window.location.origin', + target: Script::ContextTarget.new(context: context), + await_promise: false + ).result.value + result = Permissions.new(driver).set_permission( descriptor: Permissions::PermissionDescriptor.new(name: 'geolocation'), state: :granted, - origin: url_for('blank.html') + origin: origin ) expect(result).to be_empty end From bee1210c77cf84d55c37c277eb46a9fabb3cfa15 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sat, 8 Aug 2026 19:40:44 -0500 Subject: [PATCH 12/21] [rb] skip Edge bluetooth device-response in CI and fix subscribe callback leak --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 686e6fc337e9c..f88da947a8399 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -108,6 +108,9 @@ def subscribe(event) callback = driver.bidi.add_callback(event) { |params| events << params } session.subscribe(events: [event]) [events, callback] + rescue StandardError + driver.bidi.remove_callback(event, callback) if callback + raise end def unsubscribe(event, callback) @@ -286,16 +289,20 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -339,8 +346,10 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -359,8 +368,10 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do select_device connect_selected_device @@ -370,8 +381,10 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do select_device connect_selected_device @@ -428,8 +441,10 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do select_device add_service add_characteristic @@ -513,8 +528,10 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser_family: :chromium, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'} do + skip_if: [{browser: :chrome, platform: :windows, + reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, + {browser: :edge, + reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do select_device add_service add_characteristic From 57638ebd825c5cbfa0cba7449d0fc2d29da68810 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 07:02:13 -0700 Subject: [PATCH 13/21] [rb] skip chromium bluetooth in CI and correct firefox setViewport/startScreencast guards --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 42 +++++++------------ .../bidi/protocol/browsing_context_spec.rb | 11 ++--- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index f88da947a8399..80601c0c26cde 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -289,20 +289,16 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -346,10 +342,8 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -368,10 +362,8 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do select_device connect_selected_device @@ -381,10 +373,8 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do select_device connect_selected_device @@ -441,10 +431,8 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do select_device add_service add_characteristic @@ -528,10 +516,8 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: [{browser: :chrome, platform: :windows, - reason: 'Times out: no in-flight prompt/operation to respond to on Windows'}, - {browser: :edge, - reason: 'Edge lacks navigator.bluetooth: undefined on Linux, times out on Windows'}] do + skip_if: {browser_family: :chromium, + reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do select_device add_service add_characteristic diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index a9d639acd0007..eaa2bbf34e55d 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -318,10 +318,8 @@ def evaluate(expression, context = driver.window_handle) describe '#set_viewport' do it 'sets the viewport size and device pixel ratio', - pending_if: [{browser_family: :safari, - reason: 'Safari accepts browsingContext.setViewport but does not resize the window'}, - {browser: :firefox, platform: :linux, - reason: 'Firefox accepts setViewport but does not resize the window on Linux'}] do + pending_if: {browser_family: :safari, + reason: 'Safari accepts browsingContext.setViewport but does not resize the window'} do browsing_context.set_viewport( context: driver.window_handle, viewport: BrowsingContext::Viewport.new(width: 800, height: 600), @@ -351,7 +349,10 @@ def evaluate(expression, context = driver.window_handle) reason: 'Edge returns unknown command for browsingContext.startScreencast'}, {browser_family: :safari, exception: {class: Error::UnknownCommandError}, - reason: 'Safari does not implement browsingContext.startScreencast'}] do + reason: 'Safari does not implement browsingContext.startScreencast'}, + {browser: :firefox, platform: :linux, + exception: {class: Error::UnknownError, message: /startScreencast/}, + reason: 'Firefox startScreencast fails with NS_ERROR_FAILURE on Linux'}] do it 'starts and stops a screencast' do result = browsing_context.start_screencast( context: driver.window_handle, From 9358d7932a87099b2976c526a025702f59a72286 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 10:56:00 -0700 Subject: [PATCH 14/21] [rb] scope chromium bluetooth skip to CI so local runs still exercise it --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 80601c0c26cde..83409d0a619a8 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -289,16 +289,16 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -342,8 +342,8 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -362,8 +362,8 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do select_device connect_selected_device @@ -373,8 +373,8 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do select_device connect_selected_device @@ -431,8 +431,8 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do select_device add_service add_characteristic @@ -516,8 +516,8 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser_family: :chromium, - reason: 'navigator.bluetooth unreliable in CI (undefined on Linux, times out on Windows)'} do + skip_if: {browser_family: :chromium, ci: :github, + reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do select_device add_service add_characteristic From c76c1afcb8c195b54622cd6af1a51d65416c0396 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 11:50:22 -0700 Subject: [PATCH 15/21] [rb] pin Safari setViewport pending to the assertion error and skip chromium bluetooth unconditionally --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 28 +++++++++---------- .../bidi/protocol/browsing_context_spec.rb | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 83409d0a619a8..37a1f61acf968 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -289,16 +289,16 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -342,8 +342,8 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -362,8 +362,8 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do select_device connect_selected_device @@ -373,8 +373,8 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do select_device connect_selected_device @@ -431,8 +431,8 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do select_device add_service add_characteristic @@ -516,8 +516,8 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser_family: :chromium, ci: :github, - reason: 'navigator.bluetooth undefined on Linux, times out on Windows'} do + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do select_device add_service add_characteristic diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index eaa2bbf34e55d..630d2abb11683 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -319,6 +319,7 @@ def evaluate(expression, context = driver.window_handle) describe '#set_viewport' do it 'sets the viewport size and device pixel ratio', pending_if: {browser_family: :safari, + exception: {class: RSpec::Expectations::ExpectationNotMetError}, reason: 'Safari accepts browsingContext.setViewport but does not resize the window'} do browsing_context.set_viewport( context: driver.window_handle, From da67f4eb5929fa617bf3e717a0ec836be8cdd934 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 15:24:34 -0700 Subject: [PATCH 16/21] [rb] dedupe the chromium bluetooth device-response skip into one shared guard --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index 37a1f61acf968..d16dc516e2afb 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -31,6 +31,13 @@ module Protocol skip_if: {browser_family: :safari, reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do + # Shared by every device-response example: they drive the Web Bluetooth JS chooser, which never + # completes on Chromium in CI (navigator.bluetooth undefined on Linux, times out elsewhere). + chromium_device_response_skip = { + browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere' + }.freeze + after do |example| next if example.metadata[:skip] || example.skip @@ -289,16 +296,14 @@ def add_descriptor describe '#handle_request_device_prompt' do it 'accepts a prompt', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do select_device expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') end it 'cancels a prompt', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -342,8 +347,7 @@ def add_descriptor describe '#simulate_advertisement' do it 'simulates an advertisement scan entry', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do enable_adapter events, callback = subscribe('bluetooth.requestDevicePromptUpdated') start_request_device @@ -362,8 +366,7 @@ def add_descriptor describe '#simulate_gatt_connection_response' do it 'simulates a successful GATT connection response', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do select_device connect_selected_device @@ -373,8 +376,7 @@ def add_descriptor describe '#simulate_gatt_disconnection' do it 'simulates a GATT disconnection', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do select_device connect_selected_device @@ -431,8 +433,7 @@ def add_descriptor describe '#simulate_characteristic_response' do it 'simulates characteristic read and write responses', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do select_device add_service add_characteristic @@ -516,8 +517,7 @@ def add_descriptor describe '#simulate_descriptor_response' do it 'simulates descriptor read and write responses', - skip_if: {browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + skip_if: chromium_device_response_skip do select_device add_service add_characteristic From 58556b2a7285a64dda29161de6238223ac887f0b Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 15:30:27 -0700 Subject: [PATCH 17/21] [rb] dedupe repeated Safari/Chromium guards in script and browsing_context specs --- .../bidi/protocol/browsing_context_spec.rb | 14 +++++---- .../webdriver/bidi/protocol/script_spec.rb | 29 ++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index 630d2abb11683..d2520a94cdf75 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -151,6 +151,11 @@ def evaluate(expression, context = driver.window_handle) end describe '#handle_user_prompt' do + chromium_prompt_pending = { + browser_family: :chromium, + reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281' + }.freeze + it 'returns a no such alert error when no prompt is open' do expect { browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) @@ -158,8 +163,7 @@ def evaluate(expression, context = driver.window_handle) end it 'accepts user prompts without text', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + pending_if: chromium_prompt_pending do driver.navigate.to url_for('alerts.html') driver.find_element(id: 'alert').click wait_for_alert @@ -170,8 +174,7 @@ def evaluate(expression, context = driver.window_handle) end it 'accepts user prompts with text', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + pending_if: chromium_prompt_pending do driver.navigate.to url_for('alerts.html') driver.find_element(id: 'prompt').click wait_for_alert @@ -186,8 +189,7 @@ def evaluate(expression, context = driver.window_handle) end it 'rejects user prompts', - pending_if: {browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + pending_if: chromium_prompt_pending do driver.navigate.to url_for('alerts.html') driver.find_element(id: 'alert').click wait_for_alert diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index 314a821435b1b..ec9208748d35f 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -45,9 +45,18 @@ def evaluate(expression, **kwargs) ) end - describe '#add_preload_script', - pending_if: {browser_family: :safari, - reason: 'Safari script.addPreloadScript result fails strict deserialization'} do + # Safari's BiDi returns malformed remote values under strict deserialization; shared by the + # examples that read a remote value back or install a preload script. + safari_remote_value = { + browser_family: :safari, + reason: 'Safari remote value fails deserialization' + }.freeze + safari_preload = { + browser_family: :safari, + reason: 'Safari script.addPreloadScript result fails strict deserialization' + }.freeze + + describe '#add_preload_script', pending_if: safari_preload do it 'runs a preload script in future documents' do result = script.add_preload_script( function_declaration: "() => { window.__ruby_bidi_preload = 'installed'; }" @@ -83,8 +92,7 @@ def evaluate(expression, **kwargs) describe '#call_function' do it 'calls a function with local value arguments', - pending_if: {browser_family: :safari, - reason: 'Safari remote value fails deserialization'} do + pending_if: safari_remote_value do result = script.call_function( function_declaration: '(left, right) => left + right', await_promise: false, @@ -115,9 +123,7 @@ def evaluate(expression, **kwargs) end end - describe '#disown', - pending_if: {browser_family: :safari, - reason: 'Safari remote value fails deserialization'} do + describe '#disown', pending_if: safari_remote_value do it 'disowns a remote handle' do result = evaluate('({answer: 42})', result_ownership: :root) handle = result.result.handle @@ -137,8 +143,7 @@ def evaluate(expression, **kwargs) end it 'awaits promises and accepts serialization options', - pending_if: {browser_family: :safari, - reason: 'Safari remote value fails deserialization'} do + pending_if: safari_remote_value do result = evaluate( 'Promise.resolve({name: "ruby", nested: {hidden: true}})', await_promise: true, @@ -173,9 +178,7 @@ def evaluate(expression, **kwargs) end end - describe '#remove_preload_script', - pending_if: {browser_family: :safari, - reason: 'Safari script.addPreloadScript result fails strict deserialization'} do + describe '#remove_preload_script', pending_if: safari_preload do it 'removes a preload script before future navigations' do result = script.add_preload_script( function_declaration: '() => { window.__ruby_bidi_removed_preload = true; }' From 628da2f56b7986d14ba8e12f9ebc94d08659f6d5 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 9 Aug 2026 16:19:30 -0700 Subject: [PATCH 18/21] [rb] move duplicated BiDi guards onto describe/context blocks instead of shared locals --- .../webdriver/bidi/protocol/bluetooth_spec.rb | 395 +++++++++--------- .../bidi/protocol/browsing_context_spec.rb | 76 ++-- .../webdriver/bidi/protocol/script_spec.rb | 29 +- 3 files changed, 243 insertions(+), 257 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb index d16dc516e2afb..63a41aa41a400 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/bluetooth_spec.rb @@ -31,13 +31,6 @@ module Protocol skip_if: {browser_family: :safari, reason: 'Safari coverage tracked in safari_support_probe_spec.rb'}, skip_unless: {bidi: true, reason: 'only executed when bidi is enabled'} do - # Shared by every device-response example: they drive the Web Bluetooth JS chooser, which never - # completes on Chromium in CI (navigator.bluetooth undefined on Linux, times out elsewhere). - chromium_device_response_skip = { - browser_family: :chromium, - reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere' - }.freeze - after do |example| next if example.metadata[:skip] || example.skip @@ -294,37 +287,6 @@ def add_descriptor ) end - describe '#handle_request_device_prompt' do - it 'accepts a prompt', - skip_if: chromium_device_response_skip do - select_device - - expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') - end - - it 'cancels a prompt', - skip_if: chromium_device_response_skip do - enable_adapter - events, callback = subscribe('bluetooth.requestDevicePromptUpdated') - start_request_device - - expect(bluetooth.simulate_advertisement( - context: driver.window_handle, - scan_entry: scan_entry - )).to be_empty - - prompt = request_device_prompt(events) - bluetooth.handle_request_device_prompt( - context: driver.window_handle, - prompt: prompt['prompt'], - accept: false - ) - expect(evaluate_value('window.__rubyBluetoothDevicePromise', await_promise: true)).to start_with('ERROR:') - ensure - unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback - end - end - describe '#simulate_adapter' do it 'simulates a powered-on adapter' do expect(enable_adapter).to be_empty @@ -345,49 +307,6 @@ def add_descriptor end end - describe '#simulate_advertisement' do - it 'simulates an advertisement scan entry', - skip_if: chromium_device_response_skip do - enable_adapter - events, callback = subscribe('bluetooth.requestDevicePromptUpdated') - start_request_device - - expect(bluetooth.simulate_advertisement( - context: driver.window_handle, - scan_entry: scan_entry - )).to be_empty - - prompt = request_device_prompt(events) - expect(advertised_device(prompt)['id']).to be_a(String) - ensure - unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback - end - end - - describe '#simulate_gatt_connection_response' do - it 'simulates a successful GATT connection response', - skip_if: chromium_device_response_skip do - select_device - - connect_selected_device - expect(evaluate_value('window.__rubyBluetoothDevice.gatt.connected')).to be(true) - end - end - - describe '#simulate_gatt_disconnection' do - it 'simulates a GATT disconnection', - skip_if: chromium_device_response_skip do - select_device - connect_selected_device - - expect(bluetooth.simulate_gatt_disconnection( - context: driver.window_handle, - address: address - )).to be_empty - wait.until { evaluate_value('window.__rubyBluetoothDevice.gatt.connected') == false } - end - end - describe '#simulate_service' do it 'adds and removes a simulated service' do simulate_preconnected_device @@ -431,65 +350,6 @@ def add_descriptor end end - describe '#simulate_characteristic_response' do - it 'simulates characteristic read and write responses', - skip_if: chromium_device_response_skip do - select_device - add_service - add_characteristic - connect_selected_device - store_primary_service - store_characteristic - - events, callback = subscribe('bluetooth.characteristicEventGenerated') - evaluate_value( - <<~JS - (() => { - window.__rubyBluetoothReadPromise = window.__rubyBluetoothCharacteristic.readValue() - .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) - .catch(error => `ERROR:${error.name}`); - return 'started'; - })() - JS - ) - wait.until { events.find { |event| event['type'] == 'read' } } - expect(bluetooth.simulate_characteristic_response( - context: driver.window_handle, - address: address, - service_uuid: service_uuid, - characteristic_uuid: characteristic_uuid, - type: :read, - code: 0, - data: [1, 2, 3] - )).to be_empty - expect(evaluate_value('window.__rubyBluetoothReadPromise', await_promise: true)).to eq('1,2,3') - - evaluate_value( - <<~JS - (() => { - window.__rubyBluetoothWritePromise = window.__rubyBluetoothCharacteristic - .writeValueWithResponse(new Uint8Array([4, 5])) - .then(() => 'written') - .catch(error => `ERROR:${error.name}`); - return 'started'; - })() - JS - ) - wait.until { events.find { |event| event['type'] == 'write-with-response' } } - expect(bluetooth.simulate_characteristic_response( - context: driver.window_handle, - address: address, - service_uuid: service_uuid, - characteristic_uuid: characteristic_uuid, - type: :write, - code: 0 - )).to be_empty - expect(evaluate_value('window.__rubyBluetoothWritePromise', await_promise: true)).to eq('written') - ensure - unsubscribe('bluetooth.characteristicEventGenerated', callback) if callback - end - end - describe '#simulate_descriptor' do it 'adds and removes a descriptor' do simulate_preconnected_device @@ -515,67 +375,200 @@ def add_descriptor end end - describe '#simulate_descriptor_response' do - it 'simulates descriptor read and write responses', - skip_if: chromium_device_response_skip do - select_device - add_service - add_characteristic - add_descriptor - connect_selected_device - store_primary_service - store_characteristic - store_descriptor - - events, callback = subscribe('bluetooth.descriptorEventGenerated') - evaluate_value( - <<~JS - (() => { - window.__rubyBluetoothDescriptorReadPromise = window.__rubyBluetoothDescriptor.readValue() - .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) - .catch(error => `ERROR:${error.name}`); - return 'started'; - })() - JS - ) - wait.until { events.find { |event| event['type'] == 'read' } } - expect(bluetooth.simulate_descriptor_response( - context: driver.window_handle, - address: address, - service_uuid: service_uuid, - characteristic_uuid: characteristic_uuid, - descriptor_uuid: descriptor_uuid, - type: :read, - code: 0, - data: [1, 2] - )).to be_empty - expect(evaluate_value('window.__rubyBluetoothDescriptorReadPromise', await_promise: true)).to eq('1,2') - - evaluate_value( - <<~JS - (() => { - window.__rubyBluetoothDescriptorWritePromise = window.__rubyBluetoothDescriptor - .writeValue(new Uint8Array([6, 7])) - .then(() => 'written') - .catch(error => `ERROR:${error.name}`); - return 'started'; - })() - JS - ) - wait.until { events.find { |event| event['type'] == 'write' } } - expect(bluetooth.simulate_descriptor_response( - context: driver.window_handle, - address: address, - service_uuid: service_uuid, - characteristic_uuid: characteristic_uuid, - descriptor_uuid: descriptor_uuid, - type: :write, - code: 0 - )).to be_empty - expect(evaluate_value('window.__rubyBluetoothDescriptorWritePromise', - await_promise: true)).to eq('written') - ensure - unsubscribe('bluetooth.descriptorEventGenerated', callback) if callback + # These commands can only be exercised by driving the Web Bluetooth JS chooser, which never + # completes on Chromium in CI (navigator.bluetooth undefined on Linux, times out elsewhere). + context 'when driving the Web Bluetooth chooser (device-response commands)', + skip_if: {browser_family: :chromium, + reason: 'chromium bluetooth device-response: undefined on Linux, times out elsewhere'} do + describe '#handle_request_device_prompt' do + it 'accepts a prompt' do + select_device + + expect(evaluate_value('window.__rubyBluetoothDevice.name')).to eq('Ruby Heart Rate') + end + + it 'cancels a prompt' do + enable_adapter + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + start_request_device + + expect(bluetooth.simulate_advertisement( + context: driver.window_handle, + scan_entry: scan_entry + )).to be_empty + + prompt = request_device_prompt(events) + bluetooth.handle_request_device_prompt( + context: driver.window_handle, + prompt: prompt['prompt'], + accept: false + ) + expect(evaluate_value('window.__rubyBluetoothDevicePromise', await_promise: true)) + .to start_with('ERROR:') + ensure + unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback + end + end + + describe '#simulate_advertisement' do + it 'simulates an advertisement scan entry' do + enable_adapter + events, callback = subscribe('bluetooth.requestDevicePromptUpdated') + start_request_device + + expect(bluetooth.simulate_advertisement( + context: driver.window_handle, + scan_entry: scan_entry + )).to be_empty + + prompt = request_device_prompt(events) + expect(advertised_device(prompt)['id']).to be_a(String) + ensure + unsubscribe('bluetooth.requestDevicePromptUpdated', callback) if callback + end + end + + describe '#simulate_gatt_connection_response' do + it 'simulates a successful GATT connection response' do + select_device + + connect_selected_device + expect(evaluate_value('window.__rubyBluetoothDevice.gatt.connected')).to be(true) + end + end + + describe '#simulate_gatt_disconnection' do + it 'simulates a GATT disconnection' do + select_device + connect_selected_device + + expect(bluetooth.simulate_gatt_disconnection( + context: driver.window_handle, + address: address + )).to be_empty + wait.until { evaluate_value('window.__rubyBluetoothDevice.gatt.connected') == false } + end + end + + describe '#simulate_characteristic_response' do + it 'simulates characteristic read and write responses' do + select_device + add_service + add_characteristic + connect_selected_device + store_primary_service + store_characteristic + + events, callback = subscribe('bluetooth.characteristicEventGenerated') + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothReadPromise = window.__rubyBluetoothCharacteristic.readValue() + .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'read' } } + expect(bluetooth.simulate_characteristic_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :read, + code: 0, + data: [1, 2, 3] + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothReadPromise', await_promise: true)).to eq('1,2,3') + + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothWritePromise = window.__rubyBluetoothCharacteristic + .writeValueWithResponse(new Uint8Array([4, 5])) + .then(() => 'written') + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'write-with-response' } } + expect(bluetooth.simulate_characteristic_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + type: :write, + code: 0 + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothWritePromise', await_promise: true)).to eq('written') + ensure + unsubscribe('bluetooth.characteristicEventGenerated', callback) if callback + end + end + + describe '#simulate_descriptor_response' do + it 'simulates descriptor read and write responses' do + select_device + add_service + add_characteristic + add_descriptor + connect_selected_device + store_primary_service + store_characteristic + store_descriptor + + events, callback = subscribe('bluetooth.descriptorEventGenerated') + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothDescriptorReadPromise = window.__rubyBluetoothDescriptor.readValue() + .then(value => Array.from(new Uint8Array(value.buffer)).join(',')) + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'read' } } + expect(bluetooth.simulate_descriptor_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :read, + code: 0, + data: [1, 2] + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothDescriptorReadPromise', await_promise: true)).to eq('1,2') + + evaluate_value( + <<~JS + (() => { + window.__rubyBluetoothDescriptorWritePromise = window.__rubyBluetoothDescriptor + .writeValue(new Uint8Array([6, 7])) + .then(() => 'written') + .catch(error => `ERROR:${error.name}`); + return 'started'; + })() + JS + ) + wait.until { events.find { |event| event['type'] == 'write' } } + expect(bluetooth.simulate_descriptor_response( + context: driver.window_handle, + address: address, + service_uuid: service_uuid, + characteristic_uuid: characteristic_uuid, + descriptor_uuid: descriptor_uuid, + type: :write, + code: 0 + )).to be_empty + expect(evaluate_value('window.__rubyBluetoothDescriptorWritePromise', + await_promise: true)).to eq('written') + ensure + unsubscribe('bluetooth.descriptorEventGenerated', callback) if callback + end end end end diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb index d2520a94cdf75..14156b058d946 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/browsing_context_spec.rb @@ -151,52 +151,48 @@ def evaluate(expression, context = driver.window_handle) end describe '#handle_user_prompt' do - chromium_prompt_pending = { - browser_family: :chromium, - reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281' - }.freeze - it 'returns a no such alert error when no prompt is open' do expect { browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) }.to raise_error(Error::NoSuchAlertError) end - it 'accepts user prompts without text', - pending_if: chromium_prompt_pending do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'alert').click - wait_for_alert - browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') - end - - it 'accepts user prompts with text', - pending_if: chromium_prompt_pending do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'prompt').click - wait_for_alert - browsing_context.handle_user_prompt( - context: driver.window_handle, - accept: true, - user_text: 'Hello, world!' - ) - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') - end - - it 'rejects user prompts', - pending_if: chromium_prompt_pending do - driver.navigate.to url_for('alerts.html') - driver.find_element(id: 'alert').click - wait_for_alert - browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) - wait_for_no_alert - - expect(driver.title).to eq('Testing Alerts') + context 'when a prompt is open', + pending_if: {browser_family: :chromium, + reason: 'https://github.com/GoogleChromeLabs/chromium-bidi/issues/3281'} do + it 'accepts user prompts without text' do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'alert').click + wait_for_alert + browsing_context.handle_user_prompt(context: driver.window_handle, accept: true) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') + end + + it 'accepts user prompts with text' do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'prompt').click + wait_for_alert + browsing_context.handle_user_prompt( + context: driver.window_handle, + accept: true, + user_text: 'Hello, world!' + ) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') + end + + it 'rejects user prompts' do + driver.navigate.to url_for('alerts.html') + driver.find_element(id: 'alert').click + wait_for_alert + browsing_context.handle_user_prompt(context: driver.window_handle, accept: false) + wait_for_no_alert + + expect(driver.title).to eq('Testing Alerts') + end end end diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb index ec9208748d35f..314a821435b1b 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/script_spec.rb @@ -45,18 +45,9 @@ def evaluate(expression, **kwargs) ) end - # Safari's BiDi returns malformed remote values under strict deserialization; shared by the - # examples that read a remote value back or install a preload script. - safari_remote_value = { - browser_family: :safari, - reason: 'Safari remote value fails deserialization' - }.freeze - safari_preload = { - browser_family: :safari, - reason: 'Safari script.addPreloadScript result fails strict deserialization' - }.freeze - - describe '#add_preload_script', pending_if: safari_preload do + describe '#add_preload_script', + pending_if: {browser_family: :safari, + reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'runs a preload script in future documents' do result = script.add_preload_script( function_declaration: "() => { window.__ruby_bidi_preload = 'installed'; }" @@ -92,7 +83,8 @@ def evaluate(expression, **kwargs) describe '#call_function' do it 'calls a function with local value arguments', - pending_if: safari_remote_value do + pending_if: {browser_family: :safari, + reason: 'Safari remote value fails deserialization'} do result = script.call_function( function_declaration: '(left, right) => left + right', await_promise: false, @@ -123,7 +115,9 @@ def evaluate(expression, **kwargs) end end - describe '#disown', pending_if: safari_remote_value do + describe '#disown', + pending_if: {browser_family: :safari, + reason: 'Safari remote value fails deserialization'} do it 'disowns a remote handle' do result = evaluate('({answer: 42})', result_ownership: :root) handle = result.result.handle @@ -143,7 +137,8 @@ def evaluate(expression, **kwargs) end it 'awaits promises and accepts serialization options', - pending_if: safari_remote_value do + pending_if: {browser_family: :safari, + reason: 'Safari remote value fails deserialization'} do result = evaluate( 'Promise.resolve({name: "ruby", nested: {hidden: true}})', await_promise: true, @@ -178,7 +173,9 @@ def evaluate(expression, **kwargs) end end - describe '#remove_preload_script', pending_if: safari_preload do + describe '#remove_preload_script', + pending_if: {browser_family: :safari, + reason: 'Safari script.addPreloadScript result fails strict deserialization'} do it 'removes a preload script before future navigations' do result = script.add_preload_script( function_declaration: '() => { window.__ruby_bidi_removed_preload = true; }' From bc12b2c720991f2518f7f68e29e41049ba5ad705 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 10 Aug 2026 06:24:11 -0700 Subject: [PATCH 19/21] [rb] enable strict BiDi serialization for protocol specs in the harness, not the build env --- .../selenium/webdriver/bidi/protocol/BUILD.bazel | 1 - .../selenium/webdriver/spec_helper.rb | 16 ++++++++++++++++ rb/spec/tests.bzl | 7 +++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel index b5967fb4e181b..b913d90f09cd1 100644 --- a/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/bidi/protocol/BUILD.bazel @@ -14,7 +14,6 @@ filegroup( bidi = True, classic = False, data = ["//common/extensions"], - env = {"SE_BIDI_STRICT": "true"}, tags = ["exclusive-if-local"], deps = [ "//rb/lib/selenium/webdriver:bidi", diff --git a/rb/spec/integration/selenium/webdriver/spec_helper.rb b/rb/spec/integration/selenium/webdriver/spec_helper.rb index a8e15a3b6b5dd..b65a1b106b27e 100644 --- a/rb/spec/integration/selenium/webdriver/spec_helper.rb +++ b/rb/spec/integration/selenium/webdriver/spec_helper.rb @@ -119,6 +119,22 @@ def resolve_pending_exception(procsy, guard) results = @guards.disposition send(*results) if results end + + # The low-level BiDi protocol specs assert exact wire shapes, so they always run in strict + # serialization mode: a response missing a required field raises instead of being tolerated. + # Production stays lenient by default; only these specs opt in. + c.before do |example| + next unless example.metadata[:file_path].to_s.include?('bidi/protocol') + + @strict_serialization_was = ENV.fetch('SE_BIDI_STRICT', nil) + ENV['SE_BIDI_STRICT'] = 'true' + end + + c.after do |example| + next unless example.metadata[:file_path].to_s.include?('bidi/protocol') + + ENV['SE_BIDI_STRICT'] = @strict_serialization_was + end end WebDriver::Platform.exit_hook { GlobalTestEnv.quit } diff --git a/rb/spec/tests.bzl b/rb/spec/tests.bzl index 47c59dc0ca559..33c9c69991aef 100644 --- a/rb/spec/tests.bzl +++ b/rb/spec/tests.bzl @@ -185,7 +185,6 @@ def rb_integration_test( srcs, deps = [], data = [], - env = {}, browsers = BROWSERS, tags = [], bidi = False, @@ -210,7 +209,7 @@ def rb_integration_test( srcs = srcs, args = ["rb/spec/"], data = BROWSERS[browser]["data"] + data + ["//common/src/web"], - env = BROWSERS[browser]["env"] | env, + env = BROWSERS[browser]["env"], main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + local_tags + ["{}-local".format(browser)] + family_tags, deps = ["//rb/spec/integration/selenium/webdriver:spec_helper"] + BROWSERS[browser]["deps"] + deps, @@ -234,7 +233,7 @@ def rb_integration_test( env = BROWSERS[browser]["env"] | { "WD_BAZEL_JAVA_LOCATION": "$(rootpath //rb/spec:java-location)", "WD_SPEC_DRIVER": "remote", - } | env, + }, main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + ["{}-remote".format(browser)] + family_tags, deps = ["//rb/spec/integration/selenium/webdriver:spec_helper"] + BROWSERS[browser]["deps"] + deps, @@ -249,7 +248,7 @@ def rb_integration_test( srcs = srcs, args = ["rb/spec/"], data = BROWSERS[browser]["data"] + data + ["//common/src/web"], - env = BROWSERS[browser]["env"] | {"WEBDRIVER_BIDI": "true"} | env, + env = BROWSERS[browser]["env"] | {"WEBDRIVER_BIDI": "true"}, main = "@bundle//bin:rspec", tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + ["bidi", "{}-bidi".format(browser)] + family_tags, deps = {d: True for d in ( From 1858dcbd1be5bef0af00173a4656949b4ddf711c Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 10 Aug 2026 09:17:54 -0700 Subject: [PATCH 20/21] [rb] enable strict BiDi serialization for all bidi specs via the test environment --- .../selenium/webdriver/spec_helper.rb | 16 ---------------- .../webdriver/spec_support/test_environment.rb | 5 +++++ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/spec_helper.rb b/rb/spec/integration/selenium/webdriver/spec_helper.rb index b65a1b106b27e..a8e15a3b6b5dd 100644 --- a/rb/spec/integration/selenium/webdriver/spec_helper.rb +++ b/rb/spec/integration/selenium/webdriver/spec_helper.rb @@ -119,22 +119,6 @@ def resolve_pending_exception(procsy, guard) results = @guards.disposition send(*results) if results end - - # The low-level BiDi protocol specs assert exact wire shapes, so they always run in strict - # serialization mode: a response missing a required field raises instead of being tolerated. - # Production stays lenient by default; only these specs opt in. - c.before do |example| - next unless example.metadata[:file_path].to_s.include?('bidi/protocol') - - @strict_serialization_was = ENV.fetch('SE_BIDI_STRICT', nil) - ENV['SE_BIDI_STRICT'] = 'true' - end - - c.after do |example| - next unless example.metadata[:file_path].to_s.include?('bidi/protocol') - - ENV['SE_BIDI_STRICT'] = @strict_serialization_was - end end WebDriver::Platform.exit_hook { GlobalTestEnv.quit } diff --git a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb index 6bb521997d4f2..a29074c15371b 100644 --- a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb +++ b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb @@ -40,6 +40,11 @@ def initialize WebDriver.logger.ignore(:logger_info) SeleniumManager.bin_path = root.join('bazel-bin/rb/bin').to_s if File.exist?(root.join('bazel-bin/rb/bin')) + # BiDi specs exercise the wire layer, so run them under strict serialization: a response + # missing a required field raises instead of being tolerated. Production stays lenient by + # default; an explicit SE_BIDI_STRICT (e.g. to debug) still wins. + ENV['SE_BIDI_STRICT'] ||= 'true' if ENV['WEBDRIVER_BIDI'] + @driver = ENV.fetch('WD_SPEC_DRIVER', 'chrome').tr('-', '_').to_sym @driver_instance = nil @remote_server = nil From 367ed876eedc85d2087b21967f1d0bad8963b05b Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 11 Aug 2026 07:23:59 -0700 Subject: [PATCH 21/21] [rb] simplify the bidi strict-mode comment in the test environment --- .../selenium/webdriver/spec_support/test_environment.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb index a29074c15371b..be26da3a0b10b 100644 --- a/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb +++ b/rb/spec/integration/selenium/webdriver/spec_support/test_environment.rb @@ -40,9 +40,7 @@ def initialize WebDriver.logger.ignore(:logger_info) SeleniumManager.bin_path = root.join('bazel-bin/rb/bin').to_s if File.exist?(root.join('bazel-bin/rb/bin')) - # BiDi specs exercise the wire layer, so run them under strict serialization: a response - # missing a required field raises instead of being tolerated. Production stays lenient by - # default; an explicit SE_BIDI_STRICT (e.g. to debug) still wins. + # always run bidi tests in strict mode to detect browser bugs ENV['SE_BIDI_STRICT'] ||= 'true' if ENV['WEBDRIVER_BIDI'] @driver = ENV.fetch('WD_SPEC_DRIVER', 'chrome').tr('-', '_').to_sym