diff --git a/.travis.yml b/.travis.yml index 82c747e..5bec8e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,18 +25,18 @@ addons: matrix: include: - rvm: 1.9.3 - gemfile: gemfiles/2.6.gemfile + gemfile: gemfiles/2.7.gemfile env: QMAKE=/usr/lib/x86_64-linux-gnu/qt4/bin/qmake - rvm: 1.9.3 - gemfile: gemfiles/2.5.gemfile + gemfile: gemfiles/2.11.gemfile env: QMAKE=/usr/lib/x86_64-linux-gnu/qt4/bin/qmake - rvm: 2.3.1 gemfile: gemfiles/master.gemfile allow_failures: - gemfile: gemfiles/master.gemfile gemfile: - - gemfiles/2.6.gemfile - - gemfiles/2.5.gemfile + - gemfiles/2.7.gemfile + - gemfiles/2.11.gemfile before_install: - gem install bundler install: bundle diff --git a/Appraisals b/Appraisals index 1597718..ad81039 100644 --- a/Appraisals +++ b/Appraisals @@ -1,9 +1,13 @@ -appraise "2.6" do - gem "capybara", "~> 2.6.0" -end - appraise "2.7" do gem "capybara", "~> 2.7.0" + gem 'addressable', '< 2.5.0', :platforms=>[:ruby_19, :jruby_19] # 2.5 requires public_suffix which requires ruby 2.0 + gem 'nokogiri', '< 1.7.0', :platforms=>[:ruby_19, :jruby_19] # 1.7.0 requires ruby 2.1+ +end + +appraise "2.11" do + gem "capybara", "~> 2.11.0" + gem 'addressable', '< 2.5.0', :platforms=>[:ruby_19, :jruby_19] # 2.5 requires public_suffix which requires ruby 2.0 + gem 'nokogiri', '< 1.7.0', :platforms=>[:ruby_19, :jruby_19] # 1.7.0 requires ruby 2.1+ end appraise "master" do diff --git a/Gemfile.lock b/Gemfile.lock index 779b8e6..3056141 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,70 +2,63 @@ PATH remote: . specs: capybara-webkit (1.11.1) - capybara (>= 2.3.0, < 2.8.0) + capybara (>= 2.3.0, < 2.13.0) json GEM remote: https://rubygems.org/ specs: - addressable (2.3.6) - appraisal (0.4.0) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + appraisal (0.4.1) bundler rake - capybara (2.5.0) + capybara (2.11.0) + addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - childprocess (0.5.5) - ffi (~> 1.0, >= 1.0.11) - diff-lcs (1.2.4) - ffi (1.9.8) - ffi (1.9.8-java) - ffi (1.9.8-x86-mingw32) + diff-lcs (1.2.5) + ffi (1.9.14-java) json (1.8.3) - launchy (2.4.2) + json (1.8.3-java) + launchy (2.4.3) addressable (~> 2.3) - launchy (2.4.2-java) + launchy (2.4.3-java) addressable (~> 2.3) spoon (~> 0.0.1) - mime-types (2.6.1) - mini_magick (3.2.1) - subexec (~> 0.0.4) - mini_portile2 (2.0.0) - multi_json (1.11.0) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) - rack (1.6.4) - rack-protection (1.3.2) + mime-types (2.99.3) + mini_magick (4.6.0) + mini_portile2 (2.1.0) + nokogiri (1.7.0.1) + mini_portile2 (~> 2.1.0) + nokogiri (1.7.0.1-java) + nokogiri (1.7.0.1-x86-mingw32) + mini_portile2 (~> 2.1.0) + public_suffix (2.0.5) + rack (1.6.5) + rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) - rake (0.9.2) - rspec (2.14.1) - rspec-core (~> 2.14.0) - rspec-expectations (~> 2.14.0) - rspec-mocks (~> 2.14.0) - rspec-core (2.14.4) - rspec-expectations (2.14.1) + rake (11.3.0) + rspec (2.99.0) + rspec-core (~> 2.99.0) + rspec-expectations (~> 2.99.0) + rspec-mocks (~> 2.99.0) + rspec-core (2.99.2) + rspec-expectations (2.99.2) diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.14.3) - rubyzip (1.1.7) - selenium-webdriver (2.45.0) - childprocess (~> 0.5) - multi_json (~> 1.0) - rubyzip (~> 1.0) - websocket (~> 1.0) - sinatra (1.3.5) - rack (~> 1.4) - rack-protection (~> 1.3) - tilt (~> 1.3, >= 1.3.3) - spoon (0.0.4) + rspec-mocks (2.99.4) + sinatra (1.4.7) + rack (~> 1.5) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + spoon (0.0.6) ffi - subexec (0.0.4) - tilt (1.3.3) - websocket (1.2.1) + tilt (2.0.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -77,13 +70,13 @@ PLATFORMS DEPENDENCIES appraisal (~> 0.4.0) capybara-webkit! + json (< 2.0) launchy mime-types (< 3.0) mini_magick - rake - rspec (~> 2.14.0) - selenium-webdriver + rake (< 12.0.0) + rspec (~> 2.14) sinatra BUNDLED WITH - 1.11.2 + 1.13.6 diff --git a/capybara-webkit.gemspec b/capybara-webkit.gemspec index ffaf6a1..45e5720 100644 --- a/capybara-webkit.gemspec +++ b/capybara-webkit.gemspec @@ -21,16 +21,15 @@ Gem::Specification.new do |s| s.requirements << "Qt >= 4.8" - s.add_runtime_dependency("capybara", ">= 2.3.0", "< 2.8.0") + s.add_runtime_dependency("capybara", ">= 2.3.0", "< 2.13.0") s.add_runtime_dependency("json") - s.add_development_dependency("rspec", "~> 2.14.0") + s.add_development_dependency("rspec", "~> 2.14") # Sinatra is used by Capybara's TestApp s.add_development_dependency("sinatra") s.add_development_dependency("mini_magick") - s.add_development_dependency("rake") + s.add_development_dependency("rake", "< 12.0.0") s.add_development_dependency("appraisal", "~> 0.4.0") - s.add_development_dependency("selenium-webdriver") s.add_development_dependency("launchy") end diff --git a/gemfiles/2.6.gemfile b/gemfiles/2.11.gemfile similarity index 52% rename from gemfiles/2.6.gemfile rename to gemfiles/2.11.gemfile index fbd8399..625fb3f 100644 --- a/gemfiles/2.6.gemfile +++ b/gemfiles/2.11.gemfile @@ -4,7 +4,8 @@ source "https://rubygems.org" gem "mime-types", "< 3.0", :platforms=>[:ruby_19, :jruby_19] gem "json", "< 2.0", :platforms=>[:ruby_19, :jruby_19] - -gem "capybara", "~> 2.6.0" +gem "capybara", "~> 2.11.0" +gem "addressable", "< 2.5.0", :platforms=>[:ruby_19, :jruby_19] +gem "nokogiri", "< 1.7.0", :platforms=>[:ruby_19, :jruby_19] gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/2.7.gemfile b/gemfiles/2.7.gemfile index 19fbf8e..12bcf40 100644 --- a/gemfiles/2.7.gemfile +++ b/gemfiles/2.7.gemfile @@ -4,7 +4,8 @@ source "https://rubygems.org" gem "mime-types", "< 3.0", :platforms=>[:ruby_19, :jruby_19] gem "json", "< 2.0", :platforms=>[:ruby_19, :jruby_19] - gem "capybara", "~> 2.7.0" +gem "addressable", "< 2.5.0", :platforms=>[:ruby_19, :jruby_19] +gem "nokogiri", "< 1.7.0", :platforms=>[:ruby_19, :jruby_19] gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/master.gemfile b/gemfiles/master.gemfile index 4765268..4c50ac0 100644 --- a/gemfiles/master.gemfile +++ b/gemfiles/master.gemfile @@ -4,7 +4,6 @@ source "https://rubygems.org" gem "mime-types", "< 3.0", :platforms=>[:ruby_19, :jruby_19] gem "json", "< 2.0", :platforms=>[:ruby_19, :jruby_19] - gem "capybara", :github=>"jnicklas/capybara" gemspec :path=>"../" \ No newline at end of file diff --git a/lib/capybara/webkit/browser.rb b/lib/capybara/webkit/browser.rb index dc14533..9aab4b5 100644 --- a/lib/capybara/webkit/browser.rb +++ b/lib/capybara/webkit/browser.rb @@ -223,13 +223,13 @@ https://github.com/thoughtbot/capybara-webkit/wiki/Reporting-Crashes MESSAGE end - def evaluate_script(script) - json = command('Evaluate', script) + def evaluate_script(script, *args) + json = command('Evaluate', script, args.to_json) JSON.parse("[#{json}]").first end - def execute_script(script) - command('Execute', script) + def execute_script(script, *args) + command('Execute', script, args.to_json) end def render(path, width, height) diff --git a/lib/capybara/webkit/driver.rb b/lib/capybara/webkit/driver.rb index 0987afb..b16d4e6 100644 --- a/lib/capybara/webkit/driver.rb +++ b/lib/capybara/webkit/driver.rb @@ -76,13 +76,18 @@ module Capybara::Webkit @browser.title end - def execute_script(script) - value = @browser.execute_script script - value.empty? ? nil : value + def execute_script(script, *args) + value = @browser.execute_script(script, *encode_args(args)) + + if value.empty? + nil + else + value + end end - def evaluate_script(script) - @browser.evaluate_script script + def evaluate_script(script, *args) + @browser.evaluate_script(script, *encode_args(args)) end def console_messages @@ -144,6 +149,21 @@ module Capybara::Webkit end end + def switch_to_frame(frame) + case frame + when :top + begin + loop { @browser.frame_focus } + rescue Capybara::Webkit::InvalidResponseError => e + raise unless e.message =~ /Already at parent frame/ + end + when :parent + @browser.frame_focus + else + @browser.frame_focus(frame) + end + end + def within_window(selector) current_window = current_window_handle switch_to_window(selector) @@ -391,5 +411,15 @@ module Capybara::Webkit "This option is global and can be configured once" \ " (not in a `before` or `setup` block)." end + + def encode_args(args) + args.map do |arg| + if arg.is_a?(Capybara::Webkit::Node) + { ELEMENT: arg.native }.to_json + else + arg.to_json + end + end + end end end diff --git a/lib/capybara/webkit/node.rb b/lib/capybara/webkit/node.rb index 2f88f05..11619bc 100644 --- a/lib/capybara/webkit/node.rb +++ b/lib/capybara/webkit/node.rb @@ -114,11 +114,13 @@ module Capybara::Webkit end def disabled? - if %w(option optgroup).include? tag_name - self['disabled'] || find_xpath("parent::*")[0].disabled? - else - self['disabled'] - end + xpath = "parent::optgroup[@disabled] | " \ + "ancestor::select[@disabled] | " \ + "parent::fieldset[@disabled] | " \ + "ancestor::*[not(self::legend) or " \ + "preceding-sibling::legend][parent::fieldset[@disabled]]" + + self["disabled"] || !find_xpath(xpath).empty? end def path diff --git a/spec/driver_spec.rb b/spec/driver_spec.rb index c6faa6e..1ac9462 100644 --- a/spec/driver_spec.rb +++ b/spec/driver_spec.rb @@ -62,12 +62,25 @@ describe Capybara::Webkit::Driver do HTML end + + get '/iframe2' do + <<-HTML + + + Frame 2 + + +
In frame 2
+ + + HTML + end end end @@ -95,6 +108,37 @@ describe Capybara::Webkit::Driver do end end + it "switches to frame by element" do + frame = driver.find_xpath('//iframe').first + element = double(Capybara::Node::Base, base: frame) + driver.switch_to_frame(element) + driver.find_xpath("//*[contains(., 'goodbye')]").should_not be_empty + driver.switch_to_frame(:parent) + end + + it "can switch back to the parent frame" do + frame = driver.find_xpath('//iframe').first + element = double(Capybara::Node::Base, base: frame) + driver.switch_to_frame(element) + driver.switch_to_frame(:parent) + driver.find_xpath("//*[contains(., 'greeting')]").should_not be_empty + driver.find_xpath("//*[contains(., 'goodbye')]").should be_empty + end + + it "can switch to the top frame" do + frame = driver.find_xpath('//iframe').first + element = double(Capybara::Node::Base, base: frame) + driver.switch_to_frame(element) + frame2 = driver.find_xpath('//iframe[@id="g"]').first + element2 = double(Capybara::Node::Base, base: frame2) + driver.switch_to_frame(element2) + driver.find_xpath("//div[contains(., 'In frame 2')]").should_not be_empty + driver.switch_to_frame(:top) + driver.find_xpath("//*[contains(., 'greeting')]").should_not be_empty + driver.find_xpath("//*[contains(., 'goodbye')]").should be_empty + driver.find_xpath("//div[contains(., 'In frame 2')]").should be_empty + end + it "raises error for missing frame by index" do expect { driver.within_frame(1) { } }. to raise_error(Capybara::Webkit::InvalidResponseError) @@ -528,6 +572,38 @@ describe Capybara::Webkit::Driver do to raise_error(Capybara::Webkit::InvalidResponseError) end + it "passes arguments to executed Javascript" do + driver.execute_script(%, "My argument") + driver.find_xpath("//p[contains(., 'My argument')]").should_not be_empty + end + + it "passes multiple arguments to executed Javascript" do + driver.execute_script( + %, + "random", 4, {color: 'red'}) + driver.find_xpath("//p[contains(., 'random4red')]").should_not be_empty + end + + it "passes page elements to executed Javascript" do + greeting = driver.find_xpath("//p[@id='greeting']").first + driver.execute_script(%, greeting, "new content") + driver.find_xpath("//p[@id='greeting'][contains(., 'new content')]").should_not be_empty + end + + it "passes arguments to evaaluated Javascript" do + driver.evaluate_script(%, 3).should eq 3 + end + + it "passes multiple arguments to evaluated Javascript" do + driver.evaluate_script(%, 3, 4, {num: 5}).should eq 12 + end + + it "passes page elements to evaluated Javascript" do + greeting = driver.find_xpath("//p[@id='greeting']").first + driver.evaluate_script(%, "newer content", greeting, 7).should eq 7 + driver.find_xpath("//p[@id='greeting'][contains(., 'newer content')]").should_not be_empty + end + it "doesn't raise an error for Javascript that doesn't return anything" do lambda { driver.execute_script(%<(function () { "returns nothing" })()>) }. should_not raise_error @@ -726,7 +802,7 @@ describe Capybara::Webkit::Driver do @@ -764,12 +840,12 @@ describe Capybara::Webkit::Driver do end it 'finds two alert windows in a row' do - driver.accept_modal(:alert, text: 'First alert') do + driver.accept_modal(:alert, text: 'First alert') do visit('/double') end expect { - driver.accept_modal(:alert, text: 'Boom') do + driver.accept_modal(:alert, text: 'Boom') do driver.find_xpath("//input").first.click end }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with Boom" @@ -2795,7 +2871,7 @@ CACHE MANIFEST visit("/") expect(stderr).to include("http://example.com/path") - expect(stderr).not_to include(driver.current_url) + expect(stderr).not_to include(driver.current_url) end it "can block unknown hosts" do diff --git a/src/Evaluate.cpp b/src/Evaluate.cpp index fe51a69..7ab80e2 100644 --- a/src/Evaluate.cpp +++ b/src/Evaluate.cpp @@ -8,7 +8,26 @@ Evaluate::Evaluate(WebPageManager *manager, QStringList &arguments, QObject *par } void Evaluate::start() { - QVariant result = page()->currentFrame()->evaluateJavaScript(arguments()[0]); + QString script = arguments()[0]; + QString jsonArgs; + if (arguments().length()>1){ + jsonArgs = arguments()[1]; + } else { + jsonArgs ="[]"; + } + QString eval_script = QString("(function(){" + " for(var i=0; icurrentFrame()->addToJavaScriptWindowObject("CapybaraInvocation", &invocation_stub); + QVariant result = page()->currentFrame()->evaluateJavaScript(eval_script); JsonSerializer serializer; finish(true, serializer.serialize(result)); } diff --git a/src/Execute.cpp b/src/Execute.cpp index cb6fa9e..325007a 100644 --- a/src/Execute.cpp +++ b/src/Execute.cpp @@ -7,7 +7,24 @@ Execute::Execute(WebPageManager *manager, QStringList &arguments, QObject *paren } void Execute::start() { - QString script = arguments()[0] + QString("; 'success'"); + QString jsonArgs; + if (arguments().length()>1){ + jsonArgs = arguments()[1]; + } else { + jsonArgs ="[]"; + } + QString script = QString("(function(){" + " for(var i=0; icurrentFrame()->addToJavaScriptWindowObject("CapybaraInvocation", &invocation_stub); QVariant result = page()->currentFrame()->evaluateJavaScript(script); if (result.isValid()) { finish(true); diff --git a/src/FrameFocus.cpp b/src/FrameFocus.cpp index 2d24410..c6f50e9 100644 --- a/src/FrameFocus.cpp +++ b/src/FrameFocus.cpp @@ -8,7 +8,6 @@ FrameFocus::FrameFocus(WebPageManager *manager, QStringList &arguments, QObject } void FrameFocus::start() { - findFrames(); switch(arguments().length()) { case 1: focusId(arguments()[0]); @@ -26,8 +25,10 @@ void FrameFocus::findFrames() { } void FrameFocus::focusIndex(int index) { + findFrames(); if (isFrameAtIndex(index)) { frames[index]->setFocus(); + page()->setCurrentFrameParent(frames[index]->parentFrame()); success(); } else { frameNotFound(); @@ -39,9 +40,11 @@ bool FrameFocus::isFrameAtIndex(int index) { } void FrameFocus::focusId(QString name) { + findFrames(); for (int i = 0; i < frames.length(); i++) { if (frames[i]->frameName().compare(name) == 0) { frames[i]->setFocus(); + page()->setCurrentFrameParent(frames[i]->parentFrame()); success(); return; } @@ -51,10 +54,13 @@ void FrameFocus::focusId(QString name) { } void FrameFocus::focusParent() { - if (page()->currentFrame()->parentFrame() == 0) { + // if (page()->currentFrame()->parentFrame() == 0) { + if (page()->currentFrameParent() == 0) { finish(false, new ErrorMessage("Already at parent frame.")); } else { - page()->currentFrame()->parentFrame()->setFocus(); + // page()->currentFrame()->parentFrame()->setFocus(); + page()->currentFrameParent()->setFocus(); + page()->setCurrentFrameParent(page()->currentFrameParent()->parentFrame()); success(); } } diff --git a/src/JavascriptInvocation.cpp b/src/JavascriptInvocation.cpp index bd34078..ecce3c0 100644 --- a/src/JavascriptInvocation.cpp +++ b/src/JavascriptInvocation.cpp @@ -37,8 +37,14 @@ InvocationResult JavascriptInvocation::invoke(QWebFrame *frame) { QVariant result = frame->evaluateJavaScript("Capybara.invoke()"); if (getError().isValid()) return InvocationResult(getError(), true); - else + else { + if (functionName() == "leftClick") { + // Don't trigger the left click from JS incase the frame closes + QVariantMap qm = result.toMap(); + leftClick(qm["absoluteX"].toInt(), qm["absoluteY"].toInt()); + } return InvocationResult(result); + } } void JavascriptInvocation::leftClick(int x, int y) { diff --git a/src/Node.cpp b/src/Node.cpp index 6f831d1..1ffcfc5 100644 --- a/src/Node.cpp +++ b/src/Node.cpp @@ -11,6 +11,9 @@ void Node::start() { QString functionName = functionArguments.takeFirst(); QString allowUnattached = functionArguments.takeFirst(); InvocationResult result = page()->invokeCapybaraFunction(functionName, allowUnattached == "true", functionArguments); + if (functionName == "focus") { + page()->setCurrentFrameParent(page()->currentFrame()->parentFrame()); + } finish(&result); } diff --git a/src/WebPage.cpp b/src/WebPage.cpp index 05f00c4..4106bf3 100644 --- a/src/WebPage.cpp +++ b/src/WebPage.cpp @@ -21,6 +21,7 @@ WebPage::WebPage(WebPageManager *manager, QObject *parent) : QWebPage(parent) { m_uuid = QUuid::createUuid().toString(); m_confirmAction = true; m_promptAction = false; + m_currentFrameParent = 0; setForwardUnsupportedContent(true); loadJavascript(); @@ -255,6 +256,7 @@ bool WebPage::javaScriptPrompt(QWebFrame *frame, const QString &message, const Q void WebPage::loadStarted() { m_loading = true; + m_currentFrameParent = currentFrame()->parentFrame(); m_errorPageMessage = QString(); } @@ -485,3 +487,13 @@ void WebPage::addModalMessage(bool expectedType, const QString &message, const Q m_modalMessages << QString(); emit modalReady(); } + +QWebFrame* WebPage::currentFrameParent() { + return m_currentFrameParent; +} + +void WebPage::setCurrentFrameParent(QWebFrame* frame) { + m_currentFrameParent = frame; +} + + diff --git a/src/WebPage.h b/src/WebPage.h index 91ff646..89ea14b 100644 --- a/src/WebPage.h +++ b/src/WebPage.h @@ -55,6 +55,8 @@ class WebPage : public QWebPage { void resize(int, int); int modalCount(); QString modalMessage(); + void setCurrentFrameParent(QWebFrame* frame); + QWebFrame* currentFrameParent(); public slots: bool shouldInterruptJavaScript(); @@ -105,6 +107,7 @@ class WebPage : public QWebPage { QList m_modalResponses; QStringList m_modalMessages; void addModalMessage(bool, const QString &, const QRegExp &); + QWebFrame* m_currentFrameParent; }; #endif //_WEBPAGE_H diff --git a/src/capybara.js b/src/capybara.js index 2e9c933..512898b 100644 --- a/src/capybara.js +++ b/src/capybara.js @@ -5,7 +5,11 @@ Capybara = { invoke: function () { try { - return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments); + if (CapybaraInvocation.functionName == "leftClick") { + return this["verifiedClickPosition"].apply(this, CapybaraInvocation.arguments); + } else { + return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments); + } } catch (e) { CapybaraInvocation.error = e; } @@ -204,12 +208,17 @@ Capybara = { throw new Capybara.UnpositionedElement(this.pathForNode(node), visible); }, - click: function (index, action) { + verifiedClickPosition: function (index) { var node = this.getNode(index); node.scrollIntoViewIfNeeded(); var pos = this.clickPosition(node); CapybaraInvocation.hover(pos.relativeX, pos.relativeY); this.expectNodeAtPosition(node, pos); + return pos; + }, + + click: function (index, action) { + var pos = this.verifiedClickPosition(index); action(pos.absoluteX, pos.absoluteY); },