diff --git a/History.md b/History.md index 4276c7b7..3644632f 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ -#2.11.0 +#Edge +Release date: unreleased + +### Added +* Session#switch_to_frame for manually handling frame switching - Issue #1365 [Thomas Walpole] + +#Version 2.11.0 Release date: 2016-12-05 ### Added @@ -13,14 +19,14 @@ Release date: 2016-12-05 * Selenium driver with Chrome should support multiple file upload [Thomas Walpole] * Fix visible: :hidden with :text option behavior [Thomas Walpole] -#2.10.2 +#Version 2.10.2 Release date: 2016-11-30 ### Fixed * App exceptions with multiple parameter initializers now re-raised correctly - Issue #1785 [Michael Lutsiuk] * Use Addressable::URI when parsing current_path since it's more lenient of technically invalid URLs - Issue #1801 [Marcos Duque, Thomas Walpole] -#2.10.1 +#Version 2.10.1 Release date: 2016-10-08 ### Fixed @@ -28,7 +34,7 @@ Release date: 2016-10-08 * Capybara::Result optimization disabled in JRuby due to issue with lazy enumerator evaluation [Thomas Walpole] See: https://github.com/jruby/jruby/issues/4212 -#2.10.0 +#Version 2.10.0 Release date: 2016-10-05 ### Added diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index 02bfb181..06fbcf39 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -50,7 +50,7 @@ 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, :current_window, + :within, :within_fieldset, :within_table, :switch_to_frame, :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, @@ -331,6 +331,42 @@ module Capybara end end + ## + # + # Switch to the given frame + # + # If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to. + # Capybara::Session#within_frame is preferred over this method and should be used when possible. + # May not be supported by all drivers. + # + # @overload switch_to_frame(element) + # @param [Capybara::Node::Element] iframe/frame element to switch to + # @overload switch_to_frame(:parent) + # Switch to the parent element + # @overload switch_to_frame(:top) + # Switch to the top level document + # + def switch_to_frame(frame) + case frame + when Capybara::Node::Element + driver.switch_to_frame(frame) + scopes.push(:frame) + when :parent + raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's "\ + "`within` block." if scopes.last() != :frame + scopes.pop + driver.switch_to_frame(:parent) + when :top + idx = scopes.index(:frame) + if idx + raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's "\ + "`within` block." if scopes.slice(idx..-1).any? {|scope| ![:frame, nil].include?(scope)} + scopes.slice!(idx..-1) + driver.switch_to_frame(:top) + end + end + end + ## # # Execute the given block within the given iframe using given frame, frame name/id or index. @@ -344,41 +380,44 @@ module Capybara # @overload within_frame(index) # @param [Integer] index index of a frame (0 based) def within_frame(*args) - scopes.push(nil) - - frame = case args[0] - when Capybara::Node::Element - args[0] - when String, Hash - find(:frame, *args) - when Symbol - find(*args) - when Integer - idx = args[0] - all(:frame, minimum: idx+1)[idx] - else - raise ArgumentError + frame = within(document) do # Previous 2.x versions ignored current scope when finding frames - consider changing in 3.0 + case args[0] + when Capybara::Node::Element + args[0] + when String, Hash + find(:frame, *args) + when Symbol + find(*args) + when Integer + idx = args[0] + all(:frame, minimum: idx+1)[idx] + else + raise ArgumentError + end end begin - driver.switch_to_frame(frame) + switch_to_frame(frame) begin yield ensure - driver.switch_to_frame(:parent) + switch_to_frame(:parent) end rescue Capybara::NotSupportedByDriverError # Support older driver frame API for now if driver.respond_to?(:within_frame) - driver.within_frame(frame) do - yield + begin + scopes.push(:frame) + driver.within_frame(frame) do + yield + end + ensure + scopes.pop end else raise end end - ensure - scopes.pop end ## @@ -735,7 +774,9 @@ module Capybara end def current_scope - scopes.last || document + scope = scopes.last + scope = document if [nil, :frame].include? scope + scope end private diff --git a/lib/capybara/spec/session/frame/switch_to_frame_spec.rb b/lib/capybara/spec/session/frame/switch_to_frame_spec.rb new file mode 100644 index 00000000..11c68434 --- /dev/null +++ b/lib/capybara/spec/session/frame/switch_to_frame_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true +Capybara::SpecHelper.spec '#switch_to_frame', requires: [:frames] do + before(:each) do + @session.visit('/within_frames') + end + + after(:each) do + # Ensure we clean up after the frame changes + @session.switch_to_frame(:top) + end + + it "should find the div in frameOne" do + frame = @session.find(:frame, "frameOne") + @session.switch_to_frame(frame) + expect(@session.find("//*[@id='divInFrameOne']").text).to eql 'This is the text of divInFrameOne' + end + + it "should find the div in FrameTwo" do + frame = @session.find(:frame, "frameTwo") + @session.switch_to_frame(frame) + expect(@session.find("//*[@id='divInFrameTwo']").text).to eql 'This is the text of divInFrameTwo' + end + + it "should return to the parent frame when told to" do + frame = @session.find(:frame, "frameOne") + @session.switch_to_frame(frame) + @session.switch_to_frame(:parent) + expect(@session.find("//*[@id='divInMainWindow']").text).to eql 'This is the text for divInMainWindow' + end + + it "should be able to switch to nested frames" do + frame = @session.find(:frame, "parentFrame") + @session.switch_to_frame frame + frame = @session.find(:frame, "childFrame") + @session.switch_to_frame frame + frame = @session.find(:frame, "grandchildFrame1") + @session.switch_to_frame frame + expect(@session).to have_selector(:css, '#divInFrameOne', text: 'This is the text of divInFrameOne') + end + + it "should reset scope when changing frames" do + frame = @session.find(:frame, 'parentFrame') + @session.within(:css, '#divInMainWindow') do + @session.switch_to_frame(frame) + expect(@session.has_selector?(:css, "iframe#childFrame")).to be true + @session.switch_to_frame(:parent) + end + end + + it "works if the frame is closed", requires: [:frames, :js] do + frame = @session.find(:frame, 'parentFrame') + @session.switch_to_frame frame + frame = @session.find(:frame, 'childFrame') + @session.switch_to_frame frame + + @session.click_link 'Close Window' + @session.switch_to_frame :parent # Go back to parentFrame + expect(@session).to have_selector(:css, 'body#parentBody') + expect(@session).not_to have_selector(:css, '#childFrame') + @session.switch_to_frame :parent # Go back to top + end + + it "can return to the top frame", requires: [:frames] do + frame = @session.find(:frame, "parentFrame") + @session.switch_to_frame frame + frame = @session.find(:frame, "childFrame") + @session.switch_to_frame frame + @session.switch_to_frame :top + expect(@session.find("//*[@id='divInMainWindow']").text).to eql 'This is the text for divInMainWindow' + end + + it "should raise error if switching to parent unmatched inside `within` as it's nonsense" do + expect do + frame = @session.find(:frame, 'parentFrame') + @session.switch_to_frame(frame) + @session.within(:css, '#parentBody') do + @session.switch_to_frame(:parent) + end + end.to raise_error(Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's `within` block.") + end + + it "should raise error if switching to top inside a `within` in a frame as it's nonsense" do + frame = @session.find(:frame, 'parentFrame') + @session.switch_to_frame(frame) + @session.within(:css, '#parentBody') do + expect do + @session.switch_to_frame(:top) + end.to raise_error(Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's `within` block.") + end + end + + it "should raise error if switching to top inside a nested `within` in a frame as it's nonsense" do + frame = @session.find(:frame, 'parentFrame') + @session.switch_to_frame(frame) + @session.within(:css, '#parentBody') do + @session.switch_to_frame(@session.find(:frame, "childFrame")) + expect do + @session.switch_to_frame(:top) + end.to raise_error(Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's `within` block.") + @session.switch_to_frame(:parent) + end + end +end diff --git a/lib/capybara/spec/session/within_frame_spec.rb b/lib/capybara/spec/session/frame/within_frame_spec.rb similarity index 100% rename from lib/capybara/spec/session/within_frame_spec.rb rename to lib/capybara/spec/session/frame/within_frame_spec.rb