teamcapybara/capybara · error · Capybara::WindowError

block passed to #window_opened_by opened #{opened_handles.si

Error message

block passed to #window_opened_by opened #{opened_handles.size} windows instead of 1

What it means

window_opened_by snapshots driver.window_handles, runs the block, then - retrying until the wait time expires - requires exactly one new handle. Zero or multiple new windows raise Capybara::WindowError reporting the observed count. The retry loop means slow popups eventually resolve, but a popup that never appears or a burst of several windows is a hard failure.

Source

Thrown at lib/capybara/session.rb:588

    # It will wait for it to be opened (in the same way as other Capybara methods wait).
    # It's better to use this method than `windows.last`
    # {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}.
    #
    # @overload window_opened_by(**options, &block)
    #   @param options [Hash]
    #   @option options [Numeric] :wait  maximum wait time. Defaults to {Capybara.configure default_max_wait_time}
    #   @return [Capybara::Window]       the window that has been opened within a block
    #   @raise [Capybara::WindowError]   if block passed to window hasn't opened window
    #     or opened more than one window
    #
    def window_opened_by(**options)
      old_handles = driver.window_handles
      yield

      synchronize_windows(options) do
        opened_handles = (driver.window_handles - old_handles)
        if opened_handles.size != 1
          raise Capybara::WindowError, 'block passed to #window_opened_by ' \
                                       "opened #{opened_handles.size} windows instead of 1"
        end
        Window.new(self, opened_handles.first)
      end
    end

    ##
    #
    # Execute the given script, not returning a result. This is useful for scripts that return
    # complex objects, such as jQuery statements. {#execute_script} should be used over
    # {#evaluate_script} whenever possible.
    #
    # @param [String] script   A string of JavaScript to execute
    # @param args  Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
    #
    def execute_script(script, *args)
      @touched = true
      driver.execute_script(script, *driver_args(args))

View on GitHub (pinned to 15b5fdb76e)

Solutions

  1. Verify the action really opens a window (run visibly in a real browser; assert the trigger element exists and the click fired)
  2. Raise the wait: window_opened_by(wait: 5) { click_link 'Share' }
  3. If several windows legitimately open, diff manually: snapshot page.windows before/after and select the one you need
  4. For target=_blank links or window.open in the app, make sure driver options are not blocking popups

Example fix

# before
window_opened_by { click_link 'Tweet' } # WindowError: opened 0 windows

# after
window = window_opened_by(wait: 5) { click_link 'Tweet' }
within_window(window) { click_link 'Authorize' }
Defensive patterns

Strategy: retry

Validate before calling

before_handles = page.driver.window_handles
click_link 'Share'
new_handles = page.driver.window_handles - before_handles
switch_to_window(page.windows.find { |w| new_handles.include?(w.handle) }) if new_handles.size == 1

Try / catch

begin
  window_opened_by(wait: Capybara.default_max_wait_time) { click_link 'Share' }
rescue Capybara::WindowError => e
  raise "#{e.message} - open windows: #{page.windows.map(&:title).inspect}"
end

Prevention

When it happens

Trigger: window_opened_by { click_link 'Share' } where the click does not open a window (popup blocked, JS error, wrong or overlapped element), or the action opens several tabs at once; OAuth/SSO popups that exceed default_max_wait_time.

Common situations: Headless Chrome with the click intercepted by an overlay or popups effectively blocked; popup opening after the default wait (slow third-party JS); app code legitimately opening two windows; clicks matching the wrong element so nothing happens.

Related errors


AI-assisted analysis of teamcapybara/capybara@15b5fdb76e (2026-08-21). Data as JSON: /api/errors/82d9397c0d215d7b. Report an issue: GitHub.