Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Obsolete nodes are not refreshed when using Capybara's #within and/or #synchronize #242

Open
shepmaster opened this issue Sep 19, 2023 · 2 comments

Comments

@shepmaster
Copy link

Reproduction

This code uses Capybara's synchronize inside of within. The code inside synchronize always fails, but that's a simplification from the real application. The page has JavaScript that fully replaces the #parent element that contains the#child element, which is what Capybara is looking for.

Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

gem "cuprite", "~> 0.14.3"
gem "capybara", "~> 3.39"

repro.rb

require 'capybara'
require 'capybara/dsl'
require 'capybara/cuprite'

include Capybara::DSL

Capybara.javascript_driver = :cuprite
Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(app)
end

Capybara.current_driver = :cuprite
Capybara.app_host = 'http://[::1]:8888'
Capybara.run_server = false

class DummyError < StandardError; end

visit('/')

within('#parent') do
  ok_errors = page.driver.invalid_element_errors + [DummyError]
  page.document.synchronize(Capybara.default_max_wait_time, errors: ok_errors) do
    page.all('#child', count: 1)

    # We always fail this test. If everything is working correctly,
    # this program should report this exception.
    raise DummyError
  end
end

index.html

<!doctype html>
<html>
  <head>
    <title>Reproduction</title>
  </head>

  <body>
    <div id="parent">
      <div id="child">Child 1</div>
    </div>
  </body>

  <script type="text/javascript">
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function main() {
      await sleep(1000);

      const parent = document.getElementById('parent');
      const body = parent.parentElement;
      parent.remove();

      const newParent = document.createElement('div');
      newParent.id = 'parent';

      const newChild =  document.createElement('div');
      newChild.id = 'child';
      newChild.innerText = 'Child 2';

      newParent.appendChild(newChild);
      body.appendChild(newParent);
    }

    main()
  </script>
</html>

Steps

  • Serve the page via python3 -m http.server --bind '::1' 8888
  • Run the test via bundle exec ruby repro.rb

Output

gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:23:in `rescue in command': The element you are trying to interact with is either not part of the DOM, or is not currently visible on the page (perhaps display: none is set). It is possible the element has been replaced by another element and you meant to interact with the new element. If so you need to do a new find in order to get a reference to the new element. (Capybara::Cuprite::ObsoleteNode)
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:20:in `command'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:48:in `find'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:44:in `find_css'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:108:in `find_css'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:253:in `find_nodes_by_selector_format'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:166:in `block in resolve_for'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:77:in `synchronize'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:165:in `resolve_for'
	from gems/capybara-3.39.2/lib/capybara/node/finders.rb:265:in `block in all'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:77:in `synchronize'
	from gems/capybara-3.39.2/lib/capybara/node/finders.rb:264:in `all'
	from gems/capybara-3.39.2/lib/capybara/session.rb:773:in `all'
	from repro.rb:25:in `block (2 levels) in <main>'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:84:in `synchronize'
	from repro.rb:24:in `block in <main>'
	from gems/capybara-3.39.2/lib/capybara/session.rb:365:in `within'
	from gems/capybara-3.39.2/lib/capybara/dsl.rb:52:in `call'
	from gems/capybara-3.39.2/lib/capybara/dsl.rb:52:in `within'
	from repro.rb:20:in `<main>'
gems/ferrum-0.13/lib/ferrum/browser/client.rb:88:in `raise_browser_error': No node with given id found (Ferrum::NodeNotFoundError)
	from gems/ferrum-0.13/lib/ferrum/browser/client.rb:49:in `command'
	from gems/ferrum-0.13/lib/ferrum/page.rb:276:in `command'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/browser.rb:74:in `find_within'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:21:in `command'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:48:in `find'
	from gems/cuprite-0.14.3/lib/capybara/cuprite/node.rb:44:in `find_css'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:108:in `find_css'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:253:in `find_nodes_by_selector_format'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:166:in `block in resolve_for'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:77:in `synchronize'
	from gems/capybara-3.39.2/lib/capybara/queries/selector_query.rb:165:in `resolve_for'
	from gems/capybara-3.39.2/lib/capybara/node/finders.rb:265:in `block in all'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:77:in `synchronize'
	from gems/capybara-3.39.2/lib/capybara/node/finders.rb:264:in `all'
	from gems/capybara-3.39.2/lib/capybara/session.rb:773:in `all'
	from repro.rb:25:in `block (2 levels) in <main>'
	from gems/capybara-3.39.2/lib/capybara/node/base.rb:84:in `synchronize'
	from repro.rb:24:in `block in <main>'
	from gems/capybara-3.39.2/lib/capybara/session.rb:365:in `within'
	from gems/capybara-3.39.2/lib/capybara/dsl.rb:52:in `call'
	from gems/capybara-3.39.2/lib/capybara/dsl.rb:52:in `within'
	from repro.rb:20:in `<main>'

Observations

  • If I add puts page.has_selector?('#child', text: 'Child 2') before the synchronize block, it will print out true and the program will (correctly) exit with DummyError.

  • If I replace cuprite with apparition, the program will (correctly) exit with DummyError.

@infernalmaster
Copy link

I did some monkey-patching to fix similar issue

gem 'capybara', '3.37.1'
gem 'cuprite', '0.14.3'
module NodePatch
  def command(name, *args)
    browser.send(name, node, *args)
  rescue Ferrum::NodeNotFoundError => e
    # Throw ElementNotFound because it's not inside #invalid_element_errors
    # so it would not be suppresed inside Capybara::Queries::SelectorQuery#matches_filters?
    # and then error would force element to reload Capybara::Node::Element#reload
    # instead of just ignore it
    #
    # Original code:
    # raise ObsoleteNode.new(self, e.response)
    raise Capybara::ElementNotFound, e.response
  rescue Ferrum::BrowserError => e
    case e.message
    when 'Cuprite.MouseEventFailed'
      raise MouseEventFailed.new(self, e.response)
    else
      raise
    end
  end
end

Capybara::Cuprite::Node.prepend NodePatch

I still think that there should be better way to do that

@baygeldin
Copy link

I spent a lot of time debugging Capybara/Cuprite/Ferrum source code trying to get to the root of this issue just to realize it's not actually a bug. There is a good reason outlined by @jnicklas in this comment. Basically, when you use #all and the node on a page becomes obsolete, you can't be sure that if you reload results of #all they would be the same. However, it is possible to make #all results reload, just pass allow_reload option (e.g. page.all('#child', count: 1, allow_reload: true)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants