diff --git a/rb/build.desc b/rb/build.desc index f94c10ee330ec..f3d99bf37b6d1 100644 --- a/rb/build.desc +++ b/rb/build.desc @@ -291,6 +291,9 @@ ruby_library(name = "devtools", ":cdp-v84", ":cdp-v85", ":cdp-v86" + ], + resources = [ + { "../javascript/cdp-support/mutation-listener.js": "rb/lib/selenium/webdriver/atoms/mutationListener.js" } ] ) diff --git a/rb/lib/selenium/webdriver.rb b/rb/lib/selenium/webdriver.rb index 4c948cf5937a4..954b7e9345fff 100644 --- a/rb/lib/selenium/webdriver.rb +++ b/rb/lib/selenium/webdriver.rb @@ -25,8 +25,8 @@ require 'set' require 'websocket' -require 'selenium/webdriver/common' require 'selenium/webdriver/atoms' +require 'selenium/webdriver/common' require 'selenium/webdriver/version' module Selenium diff --git a/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb b/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb index 83b1909a78e2c..171b1d13f6334 100644 --- a/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +++ b/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb @@ -21,7 +21,9 @@ module Selenium module WebDriver module DriverExtensions module HasLogEvents - KINDS = %i[console exception].freeze + include Atoms + + KINDS = %i[console exception mutation].freeze # # Registers listener to be called whenever browser receives @@ -43,13 +45,19 @@ module HasLogEvents # exceptions.push(event) # end # - # @param [Symbol] kind :console or :exception + # @example Collect DOM mutations + # mutations = [] + # driver.on_log_event(:mutation) do |event| + # mutations.push(event) + # end + # + # @param [Symbol] kind :console, :exception or :mutation # @param [#call] block which is called when event happens - # @yieldparam [DevTools::ConsoleEvent, DevTools::ExceptionEvent] + # @yieldparam [DevTools::ConsoleEvent, DevTools::ExceptionEvent, DevTools::MutationEvent] # def on_log_event(kind, &block) - raise WebDriverError, "Don't know how to handle #{kind} events" unless KINDS.include?(kind) + raise Error::WebDriverError, "Don't know how to handle #{kind} events" unless KINDS.include?(kind) enabled = log_listeners[kind].any? log_listeners[kind] << block @@ -93,6 +101,42 @@ def log_exception_events end end + def log_mutation_events + devtools.page.enable + + devtools.runtime.add_binding(name: '__webdriver_attribute') + execute_script(mutation_listener) + script = devtools.page.add_script_to_evaluate_on_new_document(source: mutation_listener) + pinned_scripts[mutation_listener] = script['identifier'] + + devtools.runtime.on(:binding_called, &method(:log_mutation_event)) + end + + def log_mutation_event(params) + payload = JSON.parse(params['payload']) + elements = find_elements(css: "*[data-__webdriver_id='#{payload['target']}']") + return if elements.empty? + + event = DevTools::MutationEvent.new( + element: elements.first, + attribute_name: payload['name'], + current_value: payload['value'], + old_value: payload['oldValue'] + ) + + log_listeners[:mutation].each do |log_listener| + log_listener.call(event) + end + end + + def mutation_listener + @mutation_listener ||= read_atom(:mutationListener) + end + + def pinned_scripts + @pinned_scripts ||= {} + end + end # HasLogEvents end # DriverExtensions end # WebDriver diff --git a/rb/lib/selenium/webdriver/devtools.rb b/rb/lib/selenium/webdriver/devtools.rb index 6318a7dfebbe0..49a86233c6dec 100644 --- a/rb/lib/selenium/webdriver/devtools.rb +++ b/rb/lib/selenium/webdriver/devtools.rb @@ -22,6 +22,7 @@ module WebDriver class DevTools autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event' autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event' + autoload :MutationEvent, 'selenium/webdriver/devtools/mutation_event' SUPPORTED_VERSIONS = [84, 85, 86].freeze diff --git a/rb/lib/selenium/webdriver/devtools/mutation_event.rb b/rb/lib/selenium/webdriver/devtools/mutation_event.rb new file mode 100644 index 0000000000000..f7a2f0009bf74 --- /dev/null +++ b/rb/lib/selenium/webdriver/devtools/mutation_event.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + class DevTools + class MutationEvent + + attr_accessor :element, :attribute_name, :current_value, :old_value + + def initialize(element:, attribute_name:, current_value:, old_value:) + @element = element + @attribute_name = attribute_name + @current_value = current_value + @old_value = old_value + end + + end # MutationEvent + end # DevTools + end # WebDriver +end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/devtools_spec.rb b/rb/spec/integration/selenium/webdriver/devtools_spec.rb index 6aa07a43cf01c..189220bd416e6 100644 --- a/rb/spec/integration/selenium/webdriver/devtools_spec.rb +++ b/rb/spec/integration/selenium/webdriver/devtools_spec.rb @@ -95,6 +95,21 @@ module WebDriver expect(exception.description).to include('Error: I like cheese') expect(exception.stacktrace).not_to be_empty end + + it 'notifies about DOM mutations' do + mutations = [] + driver.on_log_event(:mutation) { |mutation| mutations.push(mutation) } + driver.navigate.to url_for('dynamic.html') + + driver.find_element(id: 'reveal').click + wait.until { mutations.any? } + + mutation = mutations.first + expect(mutation.element).to eq(driver.find_element(id: 'revealed')) + expect(mutation.attribute_name).to eq('style') + expect(mutation.current_value).to eq('') + expect(mutation.old_value).to eq('display:none;') + end end end end