From adda9b0acdfdb9280efb22dbde6ffc453ac70f55 Mon Sep 17 00:00:00 2001 From: Andrey Botalov Date: Wed, 9 Apr 2014 00:28:16 +0300 Subject: [PATCH] New API for working with windows (switching/finding/closing/opening/etc.) --- README.md | 15 ++ capybara.gemspec | 4 +- lib/capybara.rb | 3 + lib/capybara/driver/base.rb | 34 +++- lib/capybara/node/base.rb | 12 +- lib/capybara/rspec/matchers.rb | 37 ++++ lib/capybara/selenium/driver.rb | 55 ++++-- lib/capybara/session.rb | 164 +++++++++++++++++- .../spec/session/window/become_closed_spec.rb | 84 +++++++++ .../session/window/current_window_spec.rb | 24 +++ .../session/window/open_new_window_spec.rb | 28 +++ .../session/window/switch_to_window_spec.rb | 93 ++++++++++ .../session/window/window_opened_by_spec.rb | 74 ++++++++ .../spec/session/window/window_spec.rb | 114 ++++++++++++ .../spec/session/window/windows_spec.rb | 27 +++ .../spec/session/window/within_window_spec.rb | 164 ++++++++++++++++++ .../spec/session/within_window_spec.rb | 45 ----- lib/capybara/spec/spec_helper.rb | 3 +- lib/capybara/spec/views/popup_one.erb | 2 +- lib/capybara/spec/views/popup_two.erb | 2 +- lib/capybara/spec/views/with_windows.erb | 38 ++++ lib/capybara/spec/views/within_popups.erb | 25 --- lib/capybara/window.rb | 98 +++++++++++ 23 files changed, 1046 insertions(+), 99 deletions(-) create mode 100644 lib/capybara/spec/session/window/become_closed_spec.rb create mode 100644 lib/capybara/spec/session/window/current_window_spec.rb create mode 100644 lib/capybara/spec/session/window/open_new_window_spec.rb create mode 100644 lib/capybara/spec/session/window/switch_to_window_spec.rb create mode 100644 lib/capybara/spec/session/window/window_opened_by_spec.rb create mode 100644 lib/capybara/spec/session/window/window_spec.rb create mode 100644 lib/capybara/spec/session/window/windows_spec.rb create mode 100644 lib/capybara/spec/session/window/within_window_spec.rb delete mode 100644 lib/capybara/spec/session/within_window_spec.rb create mode 100644 lib/capybara/spec/views/with_windows.erb delete mode 100644 lib/capybara/spec/views/within_popups.erb create mode 100644 lib/capybara/window.rb diff --git a/README.md b/README.md index 9c383ef2..87d4f4bd 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,21 @@ within_table('Employee') do end ``` +### Working with windows + +Capybara provides some methods to ease finding and switching windows: + +```ruby +facebook_window = window_opened_by do + click_button 'Like' +end +within_window facebook_window do + find('#login_email').set('a@example.com') + find('#login_password').set('qwerty') + click_button 'Submit' +end +``` + ### Scripting In drivers which support it, you can easily execute JavaScript: diff --git a/capybara.gemspec b/capybara.gemspec index af60fdb7..0186a18c 100644 --- a/capybara.gemspec +++ b/capybara.gemspec @@ -37,13 +37,13 @@ Gem::Specification.new do |s| s.add_development_dependency("cucumber", [">= 0.10.5"]) s.add_development_dependency("rake") s.add_development_dependency("pry") - + if RUBY_ENGINE == 'rbx' then s.add_development_dependency("racc") s.add_development_dependency("json") s.add_development_dependency("rubysl") end - + if File.exist?("gem-private_key.pem") s.signing_key = 'gem-private_key.pem' end diff --git a/lib/capybara.rb b/lib/capybara.rb index 63581138..7622312e 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -13,6 +13,8 @@ module Capybara class UnselectNotAllowed < CapybaraError; end class NotSupportedByDriverError < CapybaraError; end class InfiniteRedirectError < CapybaraError; end + class ScopeError < CapybaraError; end + class WindowError < CapybaraError; end class << self attr_accessor :asset_host, :app_host, :run_server, :default_host, :always_include_port @@ -316,6 +318,7 @@ module Capybara require 'capybara/helpers' require 'capybara/session' require 'capybara/dsl' + require 'capybara/window' require 'capybara/server' require 'capybara/selector' require 'capybara/query' diff --git a/lib/capybara/driver/base.rb b/lib/capybara/driver/base.rb index 9fd20d0f..e121f08e 100644 --- a/lib/capybara/driver/base.rb +++ b/lib/capybara/driver/base.rb @@ -51,10 +51,42 @@ class Capybara::Driver::Base raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#within_frame' end - def within_window(handle) + def current_window_handle + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#current_window_handle' + end + + def current_window_size + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#window_size' + end + + def resize_current_window_to(width, height) + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#resize_window_to' + end + + def close_current_window + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#close_window' + end + + def window_handles + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#window_handles' + end + + def open_new_window + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#open_new_window' + end + + def switch_to_window(handle) + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#switch_to_window' + end + + def within_window(locator) raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#within_window' end + def no_such_window_error + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#no_such_window_error' + end + def invalid_element_errors [] end diff --git a/lib/capybara/node/base.rb b/lib/capybara/node/base.rb index d2d565a8..77740baa 100644 --- a/lib/capybara/node/base.rb +++ b/lib/capybara/node/base.rb @@ -67,10 +67,13 @@ module Capybara # Capybara will raise `Capybara::FrozenInTime`. # # @param [Integer] seconds Number of seconds to retry this block + # @param options [Hash] + # @option options [Array] :errors (driver.invalid_element_errors + + # [Capybara::ElementNotFound]) exception types that cause the block to be rerun # @return [Object] The result of the given block # @raise [Capybara::FrozenInTime] If the return value of `Time.now` appears stuck # - def synchronize(seconds=Capybara.default_wait_time) + def synchronize(seconds=Capybara.default_wait_time, options = {}) start_time = Time.now if session.synchronized @@ -82,7 +85,7 @@ module Capybara rescue => e session.raise_server_error! raise e unless driver.wait? - raise e unless catch_error?(e) + raise e unless catch_error?(e, options[:errors]) raise e if (Time.now - start_time) >= seconds sleep(0.05) raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Time.now == start_time @@ -96,8 +99,9 @@ module Capybara protected - def catch_error?(error) - (driver.invalid_element_errors + [Capybara::ElementNotFound]).any? do |type| + def catch_error?(error, errors = nil) + errors ||= (driver.invalid_element_errors + [Capybara::ElementNotFound]) + errors.any? do |type| error.is_a?(type) end end diff --git a/lib/capybara/rspec/matchers.rb b/lib/capybara/rspec/matchers.rb index 64a42f8b..7412678d 100644 --- a/lib/capybara/rspec/matchers.rb +++ b/lib/capybara/rspec/matchers.rb @@ -109,6 +109,33 @@ module Capybara end end + class BecomeClosed + def initialize(options) + @wait_time = Capybara::Query.new(options).wait + end + + def matches?(window) + @window = window + start_time = Time.now + while window.exists? && (Time.now - start_time) < @wait_time + sleep 0.05 + end + window.closed? + end + + def failure_message + "expected #{@window.inspect} to become closed after #{@wait_time} seconds" + end + + def failure_message_when_negated + "expected #{@window.inspect} not to become closed after #{@wait_time} seconds" + end + + # RSpec 2 compatibility: + alias_method :failure_message_for_should, :failure_message + alias_method :failure_message_for_should_not, :failure_message_when_negated + end + def have_selector(*args) HaveSelector.new(*args) end @@ -157,5 +184,15 @@ module Capybara def have_table(locator, options={}) HaveSelector.new(:table, locator, options) end + + ## + # Wait for window to become closed. + # @example + # expect(window).to become_closed(wait: 0.8) + # @param options [Hash] optional param + # @option options [Numeric] :wait (Capybara.default_wait_time) wait time + def become_closed(options = {}) + BecomeClosed.new(options) + end end end diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index 73ae4938..5a81dc29 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -126,24 +126,56 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) } end - def find_window( selector ) + def current_window_handle + browser.window_handle + end + + def current_window_size + size = browser.manage.window.size + [size.width, size.height] + end + + def resize_current_window_to(width, height) + browser.manage.window.resize_to(width, height) + end + + def close_current_window + browser.close + end + + def window_handles + browser.window_handles + end + + def open_new_window + browser.execute_script('window.open();') + end + + def switch_to_window(handle) + browser.switch_to.window handle + end + + # @api private + def find_window(locator) + handles = browser.window_handles + return locator if handles.include? locator + original_handle = browser.window_handle - browser.window_handles.each do |handle| + handles.each do |handle| browser.switch_to.window handle - if( selector == browser.execute_script("return window.name") || - browser.title.include?(selector) || - browser.current_url.include?(selector) || - (selector == handle) ) + if (locator == browser.execute_script("return window.name") || + browser.title.include?(locator) || + browser.current_url.include?(locator)) browser.switch_to.window original_handle return handle end end - raise Capybara::ElementNotFound, "Could not find a window identified by #{selector}" + raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}" end - def within_window(selector, &blk) - handle = find_window( selector ) - browser.switch_to.window(handle, &blk) + def within_window(locator) + handle = find_window(locator) + browser.switch_to.window(handle) { yield } end def quit @@ -158,4 +190,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base [Selenium::WebDriver::Error::StaleElementReferenceError, Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::ElementNotVisibleError] end + def no_such_window_error + Selenium::WebDriver::Error::NoSuchWindowError + end end diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index 5daf340f..3e19885e 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -40,7 +40,8 @@ module Capybara SESSION_METHODS = [ :body, :html, :source, :current_url, :current_host, :current_path, :execute_script, :evaluate_script, :visit, :go_back, :go_forward, - :within, :within_fieldset, :within_table, :within_frame, :within_window, + :within, :within_fieldset, :within_table, :within_frame, :current_window, + :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by, :save_page, :save_and_open_page, :save_screenshot, :save_and_open_screenshot, :reset_session!, :response_headers, :status_code, :title, :has_title?, :has_no_title?, :current_scope @@ -327,17 +328,162 @@ module Capybara end ## + # @return [Capybara::Window] current window # - # Execute the given block within the given window. Only works on - # some drivers (e.g. Selenium) + def current_window + Window.new(self, driver.current_window_handle) + end + + ## + # Get all opened windows. + # The order of windows in returned array is not defined. + # The driver may sort windows by their creation time but it's not required. # - # @param [String] handle of the window + # @return [Array] an array of all windows # - def within_window(handle, &blk) - scopes.push(nil) - driver.within_window(handle, &blk) - ensure - scopes.pop + def windows + driver.window_handles.map do |handle| + Window.new(self, handle) + end + end + + ## + # Open new window. + # Current window doesn't change as the result of this call. + # It should be switched to explicitly. + # + # @return [Capybara::Window] window that has been opened + # + def open_new_window + window_opened_by do + driver.open_new_window + end + end + + ## + # @overload switch_to_window(&block) + # Switches to the first window for which given block returns a value other than false or nil. + # @example + # window = switch_to_window { title == 'Page title' } + # @return [Capybara::Window] window that has been switched to + # @raise [Capybara::WindowError] if no window matches given block + # @overload switch_to_window(window) + # @param window [Capybara::Window] window that should be switched to + # @raise [Capybara::Driver::Base#no_such_window_error] if unexistent (e.g. closed) window was passed + # + # @raise [Capybara::ScopeError] if this method is invoked inside `within`, + # `within_frame` or `within_window` methods + # @raise [ArgumentError] if both or neither arguments were provided + # + def switch_to_window(window = nil) + block_given = block_given? + if window && block_given + raise ArgumentError, "`switch_to_window` can take either a block or a window, not both" + elsif !window && !block_given + raise ArgumentError, "`switch_to_window`: either window or block should be provided" + elsif scopes.size > 1 + raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\ + "`within`'s, `within_frame`'s' or `within_window`'s' block." + end + + if window + driver.switch_to_window(window.handle) + else + driver.window_handles.each do |handle| + driver.switch_to_window handle + if yield + return Window.new(self, handle) + end + end + raise Capybara::WindowError, "Could not find a window matching block/lambda" + end + end + + ## + # This method does the following: + # + # 1. Switches to the given window (it can be located by window instance/lambda/string). + # 2. Executes the given block (within window located at previous step). + # 3. Switches back (this step will be invoked even if exception will happen at second step) + # + # @overload within_window(window) { do_something } + # @param [Capybara::Window] instance of Capybara::Window class + # that will be switched to + # @overload within_window(proc_or_lambda) { do_something } + # @param lambda [Proc] lambda. First window for which lambda + # returns a value other than false or nil will be switched to. + # @example + # within_window(->{ page.title == 'Page title' }) { click_button 'Submit' } + # @raise [Capybara::WindowError] if no window matching lambda was found + # @overload within_window(string) { do_something } + # @deprecated Pass window or lambda instead + # @param [String] handle, name, url or title of the window + # + # @raise [Capybara::ScopeError] if this method is invoked inside `within`, + # `within_frame` or `within_window` methods + # @raise [driver#no_such_window_error] if unexistent (e.g. closed) window was passed + # @return value returned by the block + # + def within_window(window_or_handle) + if window_or_handle.instance_of?(Capybara::Window) + original = current_window + begin + switch_to_window(window_or_handle) unless original == window_or_handle + scopes.push(nil) + yield + ensure + scopes.pop if scopes.last.nil? # It will be not nil if closed window has been passed + switch_to_window(original) unless original == window_or_handle + end + elsif window_or_handle.is_a?(Proc) + original = current_window + begin + switch_to_window { window_or_handle.call } + scopes.push(nil) + yield + ensure + scopes.pop if scopes.last.nil? + switch_to_window(original) + end + else + offending_line = caller.first + file_line = offending_line.match(/^(.+?):(\d+)/)[0] + warn "DEPRECATION WARNING: Passing string argument to #within_window is deprecated. "\ + "Pass window object or lambda. (called from #{file_line})" + begin + scopes.push(nil) + driver.within_window(window_or_handle) { yield } + ensure + scopes.pop + end + end + end + + ## + # Get the window that has been opened by the passed block. + # 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} + # + # @param options [Hash] + # @option options [Numeric] :wait (Capybara.default_wait_time) 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 = {}, &block) + old_handles = driver.window_handles + block.call + + wait_time = Capybara::Query.new(options).wait + document.synchronize(wait_time, errors: [Capybara::WindowError]) 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 ## diff --git a/lib/capybara/spec/session/window/become_closed_spec.rb b/lib/capybara/spec/session/window/become_closed_spec.rb new file mode 100644 index 00000000..2c974bde --- /dev/null +++ b/lib/capybara/spec/session/window/become_closed_spec.rb @@ -0,0 +1,84 @@ +Capybara::SpecHelper.spec '#become_closed', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + context 'with :wait option' do + it 'should wait if value of :wait is more than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 500);') + end + Capybara.using_wait_time 0.1 do + expect(@other_window).to become_closed(wait: 0.7) + end + end + + it 'should raise error if value of :wait is less than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 700);') + end + Capybara.using_wait_time 2 do + expect do + expect(@other_window).to become_closed(wait: 0.4) + end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /\Aexpected # to become closed after 0.4 seconds\Z/) + end + end + end + + context 'without :wait option' do + it 'should wait if value of default_wait_time is more than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 500);') + end + Capybara.using_wait_time 0.7 do + expect(@other_window).to become_closed + end + end + + it 'should raise error if value of default_wait_time is less than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 700);') + end + Capybara.using_wait_time 0.4 do + expect do + expect(@other_window).to become_closed + end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /\Aexpected # to become closed after 0.4 seconds\Z/) + end + end + end + + context 'with not_to' do + it 'should raise error if default_wait_time is more than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 700);') + end + Capybara.using_wait_time 0.4 do + expect do + expect(@other_window).not_to become_closed + end + end + end + + it 'should raise error if default_wait_time is more than timeout' do + @session.within_window @other_window do + @session.execute_script('setTimeout(function(){ window.close(); }, 700);') + end + Capybara.using_wait_time 1.1 do + expect do + expect(@other_window).not_to become_closed + end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /\Aexpected # not to become closed after 1.1 seconds\Z/) + end + end + end +end diff --git a/lib/capybara/spec/session/window/current_window_spec.rb b/lib/capybara/spec/session/window/current_window_spec.rb new file mode 100644 index 00000000..9ea71261 --- /dev/null +++ b/lib/capybara/spec/session/window/current_window_spec.rb @@ -0,0 +1,24 @@ +Capybara::SpecHelper.spec '#current_window', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + it 'should return window' do + expect(@session.current_window).to be_instance_of(Capybara::Window) + end + + it "should be modified by switching to another window" do + expect do + window = @session.window_opened_by { @session.find(:css, '#openWindow').click } + @session.switch_to_window(window) + end.to change { @session.current_window } + end +end diff --git a/lib/capybara/spec/session/window/open_new_window_spec.rb b/lib/capybara/spec/session/window/open_new_window_spec.rb new file mode 100644 index 00000000..c46755a4 --- /dev/null +++ b/lib/capybara/spec/session/window/open_new_window_spec.rb @@ -0,0 +1,28 @@ +Capybara::SpecHelper.spec '#open_new_window', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + it 'should open new window with blank url and title' do + window = @session.open_new_window + @session.switch_to_window(window) + expect(['', 'about:blank']).to include(@session.title) + expect(@session.current_url).to eq('about:blank') + end + + it 'should open window with changable content' do + window = @session.open_new_window + @session.within_window window do + @session.visit '/with_html' + expect(@session).to have_css('#first') + end + end +end diff --git a/lib/capybara/spec/session/window/switch_to_window_spec.rb b/lib/capybara/spec/session/window/switch_to_window_spec.rb new file mode 100644 index 00000000..7c8d05b8 --- /dev/null +++ b/lib/capybara/spec/session/window/switch_to_window_spec.rb @@ -0,0 +1,93 @@ +Capybara::SpecHelper.spec '#switch_to_window', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + it "should raise error when invoked without args" do + expect do + @session.switch_to_window + end.to raise_error(ArgumentError, "`switch_to_window`: either window or block should be provided") + end + + it "should raise error when invoked with window and block" do + expect do + @session.switch_to_window(@window) { @session.title == 'Title of the first popup' } + end.to raise_error(ArgumentError, "`switch_to_window` can take either a block or a window, not both") + end + + context "with an instance of Capybara::Window" do + it "should be able to switch to window" do + window = @session.open_new_window + expect(@session.title).to eq('With Windows') + @session.switch_to_window(window) + expect(['', 'about:blank']).to include(@session.title) + end + end + + context "with block" do + before(:each) do + @session.find(:css, '#openTwoWindows').click + end + + it "should be able to switch to current window" do + @session.switch_to_window { @session.title == 'With Windows' } + expect(@session).to have_css('#openTwoWindows') + end + + it "should find the div in another window" do + @session.switch_to_window { @session.title == 'Title of popup two' } + expect(@session).to have_css('#divInPopupTwo') + end + + it "should be able to switch multiple times" do + @session.switch_to_window { @session.title == 'Title of the first popup' } + expect(@session).to have_css('#divInPopupOne') + @session.switch_to_window { @session.title == 'Title of popup two' } + expect(@session).to have_css('#divInPopupTwo') + end + + it "should return window" do + window = @session.switch_to_window { @session.title == 'Title of popup two' } + expect((@session.windows - [@window])).to include(window) + end + + it "should raise error when invoked inside `within` as it's nonsense" do + expect do + @session.within(:css, '#doesNotOpenWindows') do + @session.switch_to_window { @session.title == 'With Windows' } + end + end.to raise_error(Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from `within`'s, `within_frame`'s' or `within_window`'s' block.") + end + + it "should raise error when invoked inside `within_frame` as it's nonsense" do + expect do + @session.within_frame('frameOne') do + @session.switch_to_window { @session.title == 'With Windows' } + end + end.to raise_error(Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from `within`'s, `within_frame`'s' or `within_window`'s' block.") + end + + it "should raise error when invoked inside `within_window` as it's nonsense" do + window = (@session.windows - [@window]).first + expect do + @session.within_window window do + @session.switch_to_window { @session.title == 'With Windows' } + end + end.to raise_error(Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from `within`'s, `within_frame`'s' or `within_window`'s' block.") + end + end + + it "should raise error if window matching block wasn't found" do + expect do + @session.switch_to_window { @session.title == 'A title' } + end.to raise_error(Capybara::WindowError, "Could not find a window matching block/lambda") + end +end diff --git a/lib/capybara/spec/session/window/window_opened_by_spec.rb b/lib/capybara/spec/session/window/window_opened_by_spec.rb new file mode 100644 index 00000000..517998b7 --- /dev/null +++ b/lib/capybara/spec/session/window/window_opened_by_spec.rb @@ -0,0 +1,74 @@ +Capybara::SpecHelper.spec '#window_opened_by', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + let(:zero_windows_message) { "block passed to #window_opened_by opened 0 windows instead of 1" } + let(:two_windows_message) { "block passed to #window_opened_by opened 2 windows instead of 1" } + + context 'with :wait option' do + it 'should raise error if value of :wait is less than timeout' do + Capybara.using_wait_time 1 do + expect do + @session.window_opened_by(wait: 0.3) do + @session.find(:css, '#openWindowWithTimeout').click + end + end.to raise_error(Capybara::WindowError, zero_windows_message) + end + end + + it 'should find window if value of :wait is more than timeout' do + Capybara.using_wait_time 0.1 do + window = @session.window_opened_by(wait: 0.9) do + @session.find(:css, '#openWindowWithTimeout').click + end + expect(window).to be_instance_of(Capybara::Window) + end + end + end + + context 'without :wait option' do + it 'should raise error if default_wait_time is less than timeout' do + Capybara.using_wait_time 0.2 do + expect do + @session.window_opened_by do + @session.find(:css, '#openWindowWithTimeout').click + end + end.to raise_error(Capybara::WindowError, zero_windows_message) + end + end + + it 'should find window if default_wait_time is more than timeout' do + Capybara.using_wait_time 0.9 do + window = @session.window_opened_by do + @session.find(:css, '#openWindowWithTimeout').click + end + expect(window).to be_instance_of(Capybara::Window) + end + end + end + + it 'should raise error when two windows have been opened by block' do + expect do + @session.window_opened_by do + @session.find(:css, '#openTwoWindows').click + end + end.to raise_error(Capybara::WindowError, two_windows_message) + end + + it 'should raise error when no windows were opened by block' do + expect do + @session.window_opened_by do + @session.find(:css, '#doesNotOpenWindows').click + end + end.to raise_error(Capybara::WindowError, zero_windows_message) + end +end diff --git a/lib/capybara/spec/session/window/window_spec.rb b/lib/capybara/spec/session/window/window_spec.rb new file mode 100644 index 00000000..da814d02 --- /dev/null +++ b/lib/capybara/spec/session/window/window_spec.rb @@ -0,0 +1,114 @@ +Capybara::SpecHelper.spec Capybara::Window, requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + describe '#exists?' do + before(:each) do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + end + + it "should become false after window was closed" do + expect do + @session.switch_to_window @other_window + @other_window.close + end.to change { @other_window.exists? }.from(true).to(false) + end + end + + describe '#closed?' do + it "should become true after window was closed" do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + expect do + @session.switch_to_window @other_window + @other_window.close + end.to change { @other_window.closed? }.from(false).to(true) + end + end + + describe '#current?' do + before(:each) do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + end + + it 'should become true after switching to window' do + expect do + @session.switch_to_window(@other_window) + end.to change { @other_window.current? }.from(false).to(true) + end + + it 'should return false if window is closed' do + @session.switch_to_window(@other_window) + @other_window.close + expect(@other_window.current?).to be_false + end + end + + describe '#close' do + before(:each) do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + end + + it 'should change number of windows' do + expect do + @session.within_window(@other_window) do + @other_window.close + end + end.to change { @session.windows.size }.from(2).to(1) + end + + it 'should raise error if invoked not for current window' do + expect do + @other_window.close + end.to raise_error(Capybara::WindowError, "Closing not current window is not possible.") + end + end + + describe '#size' do + it 'should return size of whole window' do + expect(@session.current_window.size).to eq @session.evaluate_script("[window.outerWidth, window.outerHeight];") + end + + it 'should raise error if invoked not for current window' do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + expect do + @other_window.size + end.to raise_error(Capybara::WindowError, "Getting size of not current window is not possible.") + end + end + + describe '#resize_to' do + it 'should be able to resize window' do + width, height = @session.evaluate_script("[window.outerWidth, window.outerHeight];") + @session.current_window.resize_to(width-10, height-10) + expect(@session.evaluate_script("[window.outerWidth, window.outerHeight];")).to eq([width-10, height-10]) + end + + it 'should raise error if invoked not for current window' do + @other_window = @session.window_opened_by do + @session.find(:css, '#openWindow').click + end + expect do + @other_window.resize_to(1000, 700) + end.to raise_error(Capybara::WindowError, "Resizing not current window is not possible.") + end + end +end diff --git a/lib/capybara/spec/session/window/windows_spec.rb b/lib/capybara/spec/session/window/windows_spec.rb new file mode 100644 index 00000000..ff7728e1 --- /dev/null +++ b/lib/capybara/spec/session/window/windows_spec.rb @@ -0,0 +1,27 @@ +Capybara::SpecHelper.spec '#windows', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + @session.find(:css, '#openTwoWindows').click + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + it 'should return objects of Capybara::Window class' do + expect(@session.windows.map { |window| window.instance_of?(Capybara::Window) }).to eq([true] * 3) + end + + it 'should switchable windows' do + titles = @session.windows.map do |window| + @session.within_window(window) { @session.title } + end + expect(titles).to match_array([ + 'With Windows', 'Title of the first popup', 'Title of popup two' + ]) + end +end diff --git a/lib/capybara/spec/session/window/within_window_spec.rb b/lib/capybara/spec/session/window/within_window_spec.rb new file mode 100644 index 00000000..4df0ad5f --- /dev/null +++ b/lib/capybara/spec/session/window/within_window_spec.rb @@ -0,0 +1,164 @@ +Capybara::SpecHelper.spec '#within_window', requires: [:windows] do + before(:each) do + @window = @session.current_window + @session.visit('/with_windows') + @session.find(:css, '#openTwoWindows').click + end + after(:each) do + (@session.windows - [@window]).each do |w| + @session.switch_to_window w + w.close + end + @session.switch_to_window(@window) + end + + context "with an instance of Capybara::Window" do + it "should not invoke driver#switch_to_window when given current window" do + # switch_to_window is invoked in after hook + expect(@session.driver).to receive(:switch_to_window).exactly(3).times.and_call_original + @session.within_window @window do + expect(@session.title).to eq('With Windows') + end + end + + it "should be able to switch to another window" do + window = (@session.windows - [@window]).first + expect(@session.driver).to receive(:switch_to_window).exactly(5).times.and_call_original + @session.within_window window do + expect(['Title of the first popup', 'Title of popup two']).to include(@session.title) + end + expect(@session.title).to eq('With Windows') + end + + it "returns value from the block" do + window = (@session.windows - [@window]).first + value = @session.within_window window do + 43252003274489856000 + end + expect(value).to eq(43252003274489856000) + end + + it "should switch back if exception was raised inside block" do + window = (@session.windows - [@window]).first + expect do + @session.within_window(window) do + raise 'some error' + end + end.to raise_error(StandardError, 'some error') + expect(@session.current_window).to eq(@window) + expect(@session).to have_css('#doesNotOpenWindows') + end + + it 'should raise error if closed window was passed' do + other_window = (@session.windows - [@window]).first + @session.within_window other_window do + other_window.close + end + expect do + @session.within_window(other_window) do + raise 'should not be invoked' + end + end.to raise_error(@session.driver.no_such_window_error) + expect(@session).to have_css('#doesNotOpenWindows') + end + end + + context "with lambda" do + it "should find the div in another window" do + @session.within_window(->{ @session.title == 'Title of the first popup'}) do + expect(@session).to have_css('#divInPopupOne') + end + end + + it "should find divs in both windows" do + @session.within_window(->{ @session.title == 'Title of popup two'}) do + expect(@session).to have_css('#divInPopupTwo') + end + @session.within_window(->{ @session.title == 'Title of the first popup'}) do + expect(@session).to have_css('#divInPopupOne') + end + expect(@session.title).to eq('With Windows') + end + + it "should raise error if window wasn't found" do + expect do + @session.within_window(->{ @session.title == 'Invalid title'}) do + expect(@session).to have_css('#divInPopupOne') + end + end.to raise_error(Capybara::WindowError, "Could not find a window matching block/lambda") + expect(@session).to have_css('#doesNotOpenWindows') + end + + it "returns value from the block" do + value = @session.within_window(->{ @session.title == 'Title of popup two'}) do + 42 + end + expect(value).to eq(42) + end + + it "should switch back if exception was raised inside block" do + expect do + @session.within_window(->{ @session.title == 'Title of popup two'}) do + raise 'some error' + end + end.to raise_error(StandardError, 'some error') + expect(@session.current_window).to eq(@window) + end + end + + context "with string" do + it "should warn" do + expect(@session).to receive(:warn).with("DEPRECATION WARNING: Passing string argument "\ + "to #within_window is deprecated. Pass window object or lambda. "\ + "(called from #{__FILE__}:114)").and_call_original + @session.within_window('firstPopup') {} + end + + it "should find window by handle" do + window = (@session.windows - [@window]).first + @session.within_window window.handle do + expect(['Title of the first popup', 'Title of popup two']).to include(@session.title) + end + end + + it "should find the div in firstPopup" do + @session.within_window("firstPopup") do + expect(@session.find("//*[@id='divInPopupOne']").text).to eq 'This is the text of divInPopupOne' + end + end + it "should find the div in secondPopup" do + @session.within_window("secondPopup") do + expect(@session.find("//*[@id='divInPopupTwo']").text).to eq 'This is the text of divInPopupTwo' + end + end + it "should find the divs in both popups" do + @session.within_window("secondPopup") do + expect(@session.find("//*[@id='divInPopupTwo']").text).to eq 'This is the text of divInPopupTwo' + end + @session.within_window("firstPopup") do + expect(@session.find("//*[@id='divInPopupOne']").text).to eq 'This is the text of divInPopupOne' + end + end + it "should find the div in the main window after finding a div in a popup" do + @session.within_window("secondPopup") do + expect(@session.find("//*[@id='divInPopupTwo']").text).to eq 'This is the text of divInPopupTwo' + end + expect(@session.find("//*[@id='doesNotOpenWindows']").text).to eq 'Does not open windows' + end + it "should reset scope when switching windows" do + @session.within(:css, '#doesNotOpenWindows') do + @session.within_window("secondPopup") do + expect(@session.find("//*[@id='divInPopupTwo']").text).to eq 'This is the text of divInPopupTwo' + end + end + end + it "should switch back if exception was raised inside block" do + expect do + @session.within_window('secondPopup') do + raise 'some error' + end + end.to raise_error(StandardError, 'some error') + expect(@session.current_window).to eq(@window) + end + end +end diff --git a/lib/capybara/spec/session/within_window_spec.rb b/lib/capybara/spec/session/within_window_spec.rb deleted file mode 100644 index 9e530f87..00000000 --- a/lib/capybara/spec/session/within_window_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -Capybara::SpecHelper.spec '#within_window', :requires => [:windows] do - before(:each) do - @session.visit('/within_popups') - end - after(:each) do - @session.within_window("firstPopup") do - @session.evaluate_script('window.close()') - end - @session.within_window("secondPopup") do - @session.evaluate_script('window.close()') - end - end - - it "should find the div in firstPopup" do - @session.within_window("firstPopup") do - expect(@session.find("//*[@id='divInPopupOne']").text).to eql 'This is the text of divInPopupOne' - end - end - it "should find the div in secondPopup" do - @session.within_window("secondPopup") do - expect(@session.find("//*[@id='divInPopupTwo']").text).to eql 'This is the text of divInPopupTwo' - end - end - it "should find the divs in both popups" do - @session.within_window("secondPopup") do - expect(@session.find("//*[@id='divInPopupTwo']").text).to eql 'This is the text of divInPopupTwo' - end - @session.within_window("firstPopup") do - expect(@session.find("//*[@id='divInPopupOne']").text).to eql 'This is the text of divInPopupOne' - end - end - it "should find the div in the main window after finding a div in a popup" do - @session.within_window("secondPopup") do - expect(@session.find("//*[@id='divInPopupTwo']").text).to eql 'This is the text of divInPopupTwo' - end - expect(@session.find("//*[@id='divInMainWindow']").text).to eql 'This is the text for divInMainWindow' - end - it "should reset scope when switching windows" do - @session.within(:css, '#divInMainWindow') do - @session.within_window("secondPopup") do - expect(@session.find("//*[@id='divInPopupTwo']").text).to eql 'This is the text of divInPopupTwo' - end - end - end -end diff --git a/lib/capybara/spec/spec_helper.rb b/lib/capybara/spec/spec_helper.rb index 4f4701e4..ad4379e4 100644 --- a/lib/capybara/spec/spec_helper.rb +++ b/lib/capybara/spec/spec_helper.rb @@ -54,6 +54,7 @@ module Capybara specs = @specs RSpec.describe Capybara::Session, name, options do include Capybara::SpecHelper + include Capybara::RSpecMatchers before do @session = session end @@ -92,4 +93,4 @@ module Capybara end end -Dir[File.dirname(__FILE__)+'/session/*'].each { |group| require group } +Dir[File.dirname(__FILE__) + "/session/**/*.rb"].each { |file| require_relative file } diff --git a/lib/capybara/spec/views/popup_one.erb b/lib/capybara/spec/views/popup_one.erb index f2a79fe6..2b20a309 100644 --- a/lib/capybara/spec/views/popup_one.erb +++ b/lib/capybara/spec/views/popup_one.erb @@ -1,6 +1,6 @@ - This is the title of the first popup + Title of the first popup
This is the text of divInPopupOne
diff --git a/lib/capybara/spec/views/popup_two.erb b/lib/capybara/spec/views/popup_two.erb index c7135d74..1c32270e 100644 --- a/lib/capybara/spec/views/popup_two.erb +++ b/lib/capybara/spec/views/popup_two.erb @@ -1,6 +1,6 @@ - This is the title of popup two + Title of popup two
This is the text of divInPopupTwo
diff --git a/lib/capybara/spec/views/with_windows.erb b/lib/capybara/spec/views/with_windows.erb new file mode 100644 index 00000000..3c5e6564 --- /dev/null +++ b/lib/capybara/spec/views/with_windows.erb @@ -0,0 +1,38 @@ + + + With Windows + + + + + + + + + + + + + + + diff --git a/lib/capybara/spec/views/within_popups.erb b/lib/capybara/spec/views/within_popups.erb deleted file mode 100644 index 56c6c0e4..00000000 --- a/lib/capybara/spec/views/within_popups.erb +++ /dev/null @@ -1,25 +0,0 @@ - - - With Popups - - - -
This is the text for divInMainWindow
- - diff --git a/lib/capybara/window.rb b/lib/capybara/window.rb new file mode 100644 index 00000000..2dd36bbc --- /dev/null +++ b/lib/capybara/window.rb @@ -0,0 +1,98 @@ +module Capybara + ## + # The Window class represents a browser window. + # + # You can get an instance of the class by calling either of: + # + # * {Capybara::Session#windows} + # * {Capybara::Session#current_window} + # * {Capybara::Session#window_opened_by} + # * {Capybara::Session#switch_to_window} + # + class Window + # @return [String] a string that uniquely identifies window + attr_reader :handle + + # @return [Capybara::Session] session that this window belongs to + attr_reader :session + + # @api private + def initialize(session, handle) + @session = session + @driver = session.driver + @handle = handle + end + + ## + # @return [Boolean] whether the window is not closed + def exists? + @driver.window_handles.include?(@handle) + end + + ## + # @return [Boolean] whether the window is closed + def closed? + !exists? + end + + ## + # @return [Boolean] whether this window is the window in which commands are being executed + def current? + @driver.current_window_handle == @handle + rescue @driver.no_such_window_error + false + end + + ## + # Close window. Available only for current window. + # After calling this method future invocations of other Capybara methods should raise `session.driver.no_such_window_error` until another window will be switched to. + # @raise [Capybara::WindowError] if invoked not for current window + # + def close + raise_unless_current('Closing') + @driver.close_current_window + end + + ## + # Get window size. Available only for current window. + # @return [Array<(Fixnum, Fixnum)>] an array with width and height + # @raise [Capybara::WindowError] if invoked not for current window + # + def size + raise_unless_current('Getting size of') + @driver.current_window_size + end + + ## + # Resize window. Available only for current window. + # @param width [String] the new window width in pixels + # @param height [String] the new window height in pixels + # @raise [Capybara::WindowError] if invoked not for current window + # + def resize_to(width, height) + raise_unless_current('Resizing') + @driver.resize_current_window_to(width, height) + end + + def eql?(other) + other.kind_of?(self.class) && @session == other.session && @handle == other.handle + end + alias_method :==, :eql? + + def hash + @session.hash ^ @handle.hash + end + + def inspect + "#" + end + + private + + def raise_unless_current(what) + unless current? + raise Capybara::WindowError, "#{what} not current window is not possible." + end + end + end +end