New API for working with windows (switching/finding/closing/opening/etc.)

This commit is contained in:
Andrey Botalov 2014-04-09 00:28:16 +03:00
parent 3c5dd37a93
commit adda9b0acd
23 changed files with 1046 additions and 99 deletions

View File

@ -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:

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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<Exception>] :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

View File

@ -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

View File

@ -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

View File

@ -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<Capybara::Window>] 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
##

View File

@ -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 #<Window @handle=".+"> 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 #<Window @handle=".+"> 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 #<Window @handle=".+"> not to become closed after 1.1 seconds\Z/)
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -1,6 +1,6 @@
<html>
<head>
<title>This is the title of the first popup</title>
<title>Title of the first popup</title>
</head>
<body>
<div id="divInPopupOne">This is the text of divInPopupOne</div>

View File

@ -1,6 +1,6 @@
<html>
<head>
<title>This is the title of popup two</title>
<title>Title of popup two</title>
</head>
<body>
<div id="divInPopupTwo">This is the text of divInPopupTwo</div>

View File

@ -0,0 +1,38 @@
<html>
<head>
<title>With Windows</title>
<script src="/jquery.js" type="text/javascript" charset="utf-8"></script>
<script language="javascript" type="text/javascript">
$(document).ready(function(){
$('#openWindow').click(function(){
window.open('/popup_one', 'firstPopup');
return false;
});
$('#openWindowWithTimeout').click(function(){
setTimeout(function(){
window.open('/popup_one', 'firstPopup');
}, 500);
return false;
});
$('#openTwoWindows').click(function() {
window.open('/popup_one', 'firstPopup');
window.open('/popup_two', 'secondPopup');
return false;
});
});
</script>
</head>
<body>
<button id="openWindow">Open new window</button>
<button id="openWindowWithTimeout">Open new window with timeout</button>
<button id="openTwoWindows">Open two windows</button>
<button id="doesNotOpenWindows">Does not open windows</button>
<iframe src="/frame_one" id="frameOne"></iframe>
</body>
</html>

View File

@ -1,25 +0,0 @@
<html>
<head>
<title>With Popups</title>
<script language="javascript" type="text/javascript">
<!--
function popItUp(name, url) {
newwindow=window.open(url,name,'height=200,width=150');
if (window.focus) { newwindow.focus() }
return false;
}
function init() {
popItUp('firstPopup', '/popup_one');
popItUp('secondPopup', '/popup_two');
}
window.onload = init;
// -->
</script>
</head>
<body>
<div id="divInMainWindow">This is the text for divInMainWindow</div>
</body>
</html>

98
lib/capybara/window.rb Normal file
View File

@ -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
"#<Window @handle=#{@handle.inspect}>"
end
private
def raise_unless_current(what)
unless current?
raise Capybara::WindowError, "#{what} not current window is not possible."
end
end
end
end