Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Update to Capybara 2.4 #652

Merged
merged 12 commits into from

5 participants

@mhoran
Collaborator

Of course, just as we merged 2.3 support, Capybara 2.4 was released!

The entire suite is green with the exception of the new modal API. The biggest change is jnicklas/capybara#1322. However, I've taken a first pass at implementing the required changes and it doesn't look too bad.

@mhoran mhoran changed the title from WIP: Update Capybara to 2.4 to WIP: Update to Capybara 2.4
@jferris
Owner

Looks good so far. Do we need to add some kind of conditions around specs to make sure we're not using unsupported features when running on old versions of Capybara?

Alternatively, do we want to drop support for some of the older versions?

@mhoran
Collaborator

I have some questions about how to move forward with this. The upstream API was designed to take a block in order to support "proactive" modal management, which applies to us. See the comments in jnicklas/capybara#1037 and #506. @jferris and @mikepack did most of the legwork on looking into whether the "reactive" model would be possible with capybara-webkit, with comments in #506.

As @jferris points out in #506, it may be possible to support "reactive" modal management in capybara-webkit, but it would take some significant refactoring. I spent some time looking into the possibility of this, and in addition to the cases outlined in #506, we would also have to support a way to get the status of commands in an asynchronous manner, e.g. when calling Node#invoke, there may not be a return value to a JavaScript interaction until a modal is acknowledged or dismissed. This is a significant change.

The reason I even bring up "reactive" modal management is that the "proactive" model seems flawed in the case that:

  1. an unexpected modal is displayed (we could verify that the modal text matches in webkit_server and only accept the prompt if so, and if it does not match, dismiss)
  2. accepting or dismissing a modal causes a follow-up modal to be displayed (not a great user interaction, but also not possible to test in a "proactive" model so far as I can tell.) I would imagine a Capybara user may try to write something like:
accept_confirm do
  dismiss_confirm do
    # action that causes modal to be displayed
  end
  # inner modal would have been dismissed, but outer modal would have been dismissed
  # as well due to the "proactive" API
end

I'd prefer to avoid the complexity associated with "reactive" modal management, however I also want to ensure that the API behaves as expected. One thing that will be different with a "proactive" API is that an unexpected modal must either be accepted or dismissed regardless of whether the modal matches the selector. This is because the modal must be handled synchronously, and if the modal does not match the selector a default action must be taken. We currently default to accepting confirms and dismissing prompts. This behavior could be persevered, with the addition of a check for selector matching, but the case outlined in 2 will still not be possible to test.

@jferris
Owner

I didn't have much luck getting capybara-webkit to be reactive. I still believe that it's possible, but there are some hurdles:

  • Much of our existing code makes unsafe assumptions about threads, so we need a decent number of mutexes, etc to prevent unsafe access. Although I think getting the code mostly right would be fast, I'm concerned that we don't have adequate testing or debugging tools to make this feasible. We already have enough trouble hunting down NULL pointer exceptions.
  • Qt's GUI code must live in the main event loop and threads are not allowed to post to each others' event loops. Having the connection live in a background thread but the browser run in the main thread requires some gymnastics to coordinate. Qt provides some utilities for facilitating these interactions, but they're not without complications.

I don't think the code base is currently in a happy enough state that we could reasonably hope to achieve the massive changes required to support async commands.

A few ideas:

  • Match compatibility with the Selenium driver as much as possible without being reactive.
  • Preserve backwards compatibility (auto-accept dialogs) with a deprecation warning.
  • Raise an error for unhandled dialogs in a future version.
@mhoran
Collaborator

@twalpole, implementation of the modal API for capybara-webkit is coming along nicely. I've got a few questions about how the API should behave that are not specified in the upstream integration tests.

  1. Should any unexpected modal raise an error? e.g. when not within an accept_confirm or dismiss_confirm block, should the presence of a modal raise an exception, or continue in a default manner? I'm not sure how Selenium behaves in this case, but our current behavior is to accept confirms by default, but reject prompts. Also, if a modal of a different type is displayed, what should we do? e.g. within accept_confirm an alert is displayed instead.

  2. Is nesting of the accept and dismiss API allowed? e.g. can one call dismiss_confirm passing in accept_confirm in a block? I've implemented this in capybara-webkit, but it's a bit hairy and I'm not sure what the Selenium behavior is in this case.

Thanks!

@twalpole

@mhoran

1. Selenium raises a Selenium::Webdriver::Error::UnhandledAlertError if any page interaction is attempted (other than dealing with the modal) while a modal is active. I think it would be good if other drivers behaved similarly (at appearance time of an unexpected modal would be fine if that is easiest), although we could delay guaranteeing that behavior for a future version so as not to immediately break existing tests if wanted. Unfortunately Selenium does not differentiate between the different modal types, so there is no way for me to raise an error on an incorrect modal type, although that would have been my preferred behavior.
2. I had not thought about the nesting of modals, although I believe it should work correctly with the selenium driver (will check in a few minutes) and should probably be supported

@twalpole

@mhoran jnicklas/capybara@024b20b that test for nested behavior passes on selenium -- I can add it in to a 2.4.2 release along with tests for any other clarification of modal behavior that are needed if wanted

@abotalov

Selenium raises a Selenium::Webdriver::Error::UnhandledAlertError if any page interaction is attempted (other than dealing with the modal) while a modal is active.

That's not fully true. The following is from Webdriver W3C spec - it's in draft state and the following may not be currently fully implemented by Selenium:

If a modal dialog is created from a onbeforeunload event the remote end must handle the dialog by either using accept or dismiss. These calls should either come from the local end or should be handled as an unexpected modal dialog as described below.
The remote end should have a mechanism to allow unexpected modal dialogs to be closed to prevent the remote end from becoming unusable. The default for this should be dismiss. The local end should allow a capability to be set that allows the default value to be overridden with accept. The local end should also allow setting the default behaviour to wait for a command to handle the modal. If the next command does not interact with the modal it must return a Unexpected alert open error to the local end.

Also there were some discussions among selenium developers about changing those capabilities. (see e.g. https://www.w3.org/Bugs/Public/show_bug.cgi?id=25148, https://groups.google.com/forum/#!topic/selenium-developers/8vCYEeFMpfA). Don't know if they came up with a decision.

@twalpole

@abotalov -- Since the question was basically "What does the selenium driver do?" - how does pasting a spec that it may or may not implement help? What part of my previous answer is not correct, so that @mhoran can attempt to match that behavior?

@abotalov

Your previous comment illustrates default behavior of Selenium.

However, it seems (I haven't tried it) that this behavior can be changed using "unexpectedAlertBehaviour" (see https://github.com/SeleniumHQ/selenium/blob/master/java/client/src/org/openqa/selenium/UnexpectedAlertBehaviour.java). Ruby binding doesn't seem to have such interface yet.

I commented just to note that Selenium also has a concept of automatic dealing with alerts.

@twalpole

and the firefox driver for selenium implements that spec as - If a command attempts to interact with the page while a modal is active, it will do the default behavior and then raise an error - so the entire point of the spec is a way for the browser to not be left in an unusable state - it is not for the browser to apply a default behavior to the modal and then continue on as if the modal never appeared.

@mhoran
Collaborator

Thanks @twalpole, @abotalov. I think I have what I need to move forward here.

@jferris, I've done what I can to keep the API backwards compatible. I think the best thing to do would be to deprecate the our legacy prompt and confirm API in the next release, to be removed in the next major release, as you suggested. Once the legacy API is removed, we can raise errors in the event that unexpected modals are encountered.

@jferris
Owner

@mhoran that sounds good. We may want to use the 2.4-compatible release as an opportunity to deprecate a bunch of capybara-webkit-specific APIs which have been replaced by official ones. We have a few different ones for windows, modals, console messages, and screenshots.

@mhoran
Collaborator

@jferris, agreed. I've added deprecation warnings for the capybara-specific modal APIs. The old window API has already been deprecated upstream, though we have two aliases for an older API that I'll add deprecation warnings for. We already renamed Driver#render to Driver#save_screenshot. I don't believe there's an upstream API for reading console messages at this time.

Looks good so far. Do we need to add some kind of conditions around specs to make sure we're not using unsupported features when running on old versions of Capybara?

I ended up having to do this to the Selenium compatibility spec as readonly input interaction has changed in Capybara 2.4. We now only run this spec against Capybara 2.4 and skip it on other versions.

I also had to stub Capybara::ModalNotFound to get our tests running in Capybara < 2.4. Given that no upstream code path will expose these methods, users shouldn't get a NameError. We could also conditionally exclude these tests for Capybara < 2.4 if you think that would be better. I figured the integration tests would catch any issues with upstream renaming of Capybara::ModalNotFound.

Alternatively, do we want to drop support for some of the older versions?

I've also removed Capybara 2.1 from the Travis config, so tests will only run against 2.2, 2.3 and 2.4. I'm happy dropping 2.2 as well, though I think official support for at least one previous version is important to ensure a stable upgrade path.

@jferris
Owner

This all sounds good to me. I think stubbing ModalNotFound is easier than filtering the tests. I think it's fine to just ensure Selenium compatibility with the latest version. Also, I think 2.2+ is probably good for this release; we were stuck on 2.1 for a while, so it's probably good to give users a little extra time to update capybara/capybara-webkit.

@mhoran mhoran changed the title from WIP: Update to Capybara 2.4 to Update to Capybara 2.4
@mhoran
Collaborator

We should be good to go here. I've added a bunch of deprecation warnings and removed deprecated methods from the README. @jferris, if you could review when you get a chance that'd be great!

mhoran added some commits
@mhoran mhoran Implement modal (confirm, prompt and alert) API
* Retain backwards compatibility with legacy capybara-webkit API.
* Confirm dialogs are accepted by default; dialogs are dismissed.
* Legacy API overrides the default action, and does not raise errors
  for unexpected modals.
b411f53
@mhoran mhoran Travis config for Capybara 2.4 5b43f53
@mhoran mhoran Don't interact with readonly elements
* This behavior changed in Capybara 2.4.
* Previously we would focus and send keypress events to readonly
  elements. Now readonly elements are ignored, and a warning is emitted
  by Capybara.
bbcfb7e
@mhoran mhoran Add deprecation warnings to legacy modal methods
* Our legacy API has been replaced by an upstream API in Capybara 2.4.
8ba5ddf
@mhoran mhoran Add Capybara 2.4 to Appraisals 6cd0348
@mhoran mhoran Stub Capybara::ModalNotFound for Capybara < 2.4
* The modal API was introduced in Capybara 2.4, so older versions won't
  follow this code path.
e49886d
lib/capybara/webkit/driver.rb
@@ -140,26 +150,38 @@ def close_window(selector)
end
def maximize_window(selector)
+ warn '[DEPRECATION] Capybara::Webkit::Driver#maximize_window ' \
@jferris Owner
jferris added a note

This is part of the core API, so I don't think it should be deprecated: http://rubydoc.info/gems/capybara/Capybara/Driver/Base#maximize_window-instance_method

@mhoran Collaborator
mhoran added a note

@jferris, good catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jferris jferris commented on the diff
lib/capybara/webkit/driver.rb
@@ -89,6 +97,8 @@ def status_code
end
def resize_window(width, height)
@jferris Owner
jferris added a note

This is part of the core API, so I don't think it should be deprecated: http://rubydoc.info/gems/capybara/Capybara/Driver/Base:resize_window_to

@jferris Owner
jferris added a note

Oh, no it's not; the core on is "resize_window_to." Please disregard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
mhoran added some commits
@mhoran mhoran Add deprecation warnings to legacy window methods
* Capybara 2.3 provides a new window API.
99f4a88
@mhoran mhoran Update README
* Remove deprecated modal and window commands.
* Document xvfb-run as a way to start an X server.
* Note that Qt versions greater than 4.8 are supported.
* Update copyright year.
d7f724d
@jferris jferris commented on the diff
lib/capybara/webkit/driver.rb
@@ -217,5 +269,32 @@ def version
browser.version
].join("\n")
end
+
+ private
+
+ def modal_action_options_for_browser(options)
+ if options[:text].is_a?(Regexp)
+ options.merge(text: options[:text].source)
+ else
+ options.merge(text: Regexp.escape(options[:text].to_s))
+ end.merge(original_text: options[:text])
+ end
+
+ def find_modal(type, id, options)
+ Timeout::timeout(options[:wait] || Capybara.default_wait_time) do
@jferris Owner
jferris added a note

It's odd that Capybara doesn't handle this, as it generally does with nodes and windows. Does Capybara expect find_modal to return the correct instance immediately?

find_modal isn't part of the Capybara API - because of the proactive/reactive nature of dealing with modals Capybara doesnt really provide anything for dealing with them, other than the external API definition

@mhoran Collaborator
mhoran added a note

Note that the window management APIs do not retry. If the title of the page does not match at the time of execution, window switching (or finding) will fail.

@mhorn window_opened_by retries until the new window is available (or timeout), and is now recommended as the preferred way of dealing with new windows opened during interaction

@mhoran Collaborator
mhoran added a note

Thanks for the clarification, @twalpole. I see that now -- I had only looked at within_window and switch_to_window.

@jferris, inspiration for find_modal came from the Selenium Driver implementation: https://github.com/jnicklas/capybara/blob/master/lib/capybara/selenium/driver.rb#L254. If we had a handle to a Document we could use synchronize as window_opened_by does. I'm not sure if that's possible, or desired.

@mhoran Collaborator
mhoran added a note

@jferris, I looked into making FindModal synchronous this evening but ran into an issue when the timeout is reached. The exception raised by Timeout::timeout results in a Reset being sent before FindModal has finished. Our Connection will start a command so long as the page is not loading -- which is desired in the case of Reset. However, this makes it possible for two commands to emit finished (and other signals), which results in undesired behavior.

I looked into stopping all pending commands when Reset is received, which fixes the issue. However, I'm not thrilled with what I came up with, and it's a bit of a rework that I'm not sure we should bundle in with Capybara 2.4 compatibility. However, I've definitely seen issues with simultaneous command processing in the past and it'd be a nice fix to have.

I'm happy to spend more time on this if you think it's worth getting synchronous FindCommand for this release, but if you're OK with polling for the interim I think that may be a safer bet while we come up with a polished solution.

@jferris Owner
jferris added a note

I think we should do what's reasonable to get Capybara 2.4 support out; it will be nice to get caught up.

After the release, though, I think it makes sense to try and make the server act synchronously with commands. I actually thought this was already the case, and I can't think of a reason you'd really want another command to go through before the previous command was finished.

@jferris Owner
jferris added a note

Looking at this a little more:

There's nothing on the C++ side which will prevent multiple commands being received and acted on, but the Ruby driver shouldn't ever do so. In browser.rb, it always waits for a response before returning from command, so unless the C++ command emits a response before it's actually finished, multiple commands can't execute simultaneously. Are you seeing behavior where that isn't the case?

@mhoran Collaborator
mhoran added a note

When an exception is raised in Ruby and a pending command has not finished (which would happen in the case of synchronous FindModal), Ruby will not wait for the command to finish before issuing a Reset. The reset will likely succeed, but if a subsequent test tries to use the driver, the outstanding command may still be running. Looking at the debug output, for example, you'll see two "started page load" messages printed, one for the outstanding command, and one for the subsequent command.

I think it's a good thing that Reset can be sent while a pending command is running, especially in the case of exceptions. However, we don't currently handle this very well and should probably make that code more robust. I tried this in https://github.com/mhoran/capybara-webkit/commits/synchronous_find_modal.

@mhoran Collaborator
mhoran added a note

I found a much easier way to ensure that we don't process two commands in the event of an exception/Reset cycle. However, in making FindModal synchronous I discovered a bug in JRuby: Timeout::timeout blocks when reading from a socket. This results in the timeout block executing, raising an exception after the block returns if the timeout is reached, but never timing out in the case that the underlying call blocks. This is an issue for synchronous FindCommand.

@mhoran Collaborator
mhoran added a note

cc6b910 resolves the JRuby timeout issue. I've pushed synchronous FindModal to this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
mhoran added some commits
@mhoran mhoran Delete pending commands on reset
* If an exception is raised in the Ruby process, it's possible for Reset
  to be sent to the server while the previous command is still running.
  This ensures that pending commands are stopped when handling Reset.
64762b6
@mhoran mhoran Make FindModal synchronous
* There's no need to poll for the modal when we have an event loop.
8dbf3b8
@mhoran mhoran Make Connection#gets non-blocking
* JRuby Timeout::timeout blocks on IO#gets. IO.connect blocks as well
  unless running in its own thread.
cc6b910
@mhoran mhoran Make Connection#read non-blocking
* Ensure that Connection#read will not block Timeout::timeout for JRuby
  users.
22a81f6
@mike-burns mike-burns commented on the diff
gemfiles/2.4.gemfile
@@ -0,0 +1,7 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "capybara", "~> 2.4.0"
+
+gemspec :path=>"../"
@mike-burns Owner

Missing newline terminator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mhoran
Collaborator

@jferris, any further thoughts on this? With 8dbf3b8 I've removed the retry logic from find_modal and made the underlying Command synchronous. This required a workaround for JRuby, which I found would block Timeout::timeout (see jruby/jruby#1831). A workaround for JRuby can be found in cc6b910.

I looked into moving the timeout into webkit_server, but this required reworking TimeoutCommand and PageLoadingCommand and I ran into some deadlocks. It also didn't really make the code much cleaner and changes the behavior of Browser#timeout=. I think this may be something worth doing in the long run, but I think we can work with this for now.

@jferris
Owner

@mhoran sorry, I've been on vacation for the past couple weeks and away from email. I think this looks good to merge and would be great to release.

@mhoran mhoran merged commit 22a81f6 into thoughtbot:master
@mhoran mhoran deleted the mhoran:mh_update_capybara branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 13, 2014
  1. @mhoran

    Implement modal (confirm, prompt and alert) API

    mhoran authored
    * Retain backwards compatibility with legacy capybara-webkit API.
    * Confirm dialogs are accepted by default; dialogs are dismissed.
    * Legacy API overrides the default action, and does not raise errors
      for unexpected modals.
  2. @mhoran

    Travis config for Capybara 2.4

    mhoran authored
  3. @mhoran

    Don't interact with readonly elements

    mhoran authored
    * This behavior changed in Capybara 2.4.
    * Previously we would focus and send keypress events to readonly
      elements. Now readonly elements are ignored, and a warning is emitted
      by Capybara.
  4. @mhoran

    Add deprecation warnings to legacy modal methods

    mhoran authored
    * Our legacy API has been replaced by an upstream API in Capybara 2.4.
  5. @mhoran

    Add Capybara 2.4 to Appraisals

    mhoran authored
  6. @mhoran

    Stub Capybara::ModalNotFound for Capybara < 2.4

    mhoran authored
    * The modal API was introduced in Capybara 2.4, so older versions won't
      follow this code path.
Commits on Jul 14, 2014
  1. @mhoran

    Add deprecation warnings to legacy window methods

    mhoran authored mhoran committed
    * Capybara 2.3 provides a new window API.
  2. @mhoran

    Update README

    mhoran authored mhoran committed
    * Remove deprecated modal and window commands.
    * Document xvfb-run as a way to start an X server.
    * Note that Qt versions greater than 4.8 are supported.
    * Update copyright year.
Commits on Jul 17, 2014
  1. @mhoran

    Delete pending commands on reset

    mhoran authored
    * If an exception is raised in the Ruby process, it's possible for Reset
      to be sent to the server while the previous command is still running.
      This ensures that pending commands are stopped when handling Reset.
  2. @mhoran

    Make FindModal synchronous

    mhoran authored
    * There's no need to poll for the modal when we have an event loop.
  3. @mhoran

    Make Connection#gets non-blocking

    mhoran authored
    * JRuby Timeout::timeout blocks on IO#gets. IO.connect blocks as well
      unless running in its own thread.
Commits on Jul 18, 2014
  1. @mhoran

    Make Connection#read non-blocking

    mhoran authored
    * Ensure that Connection#read will not block Timeout::timeout for JRuby
      users.
This page is out of date. Refresh to see the latest.
Showing with 740 additions and 173 deletions.
  1. +4 −4 .travis.yml
  2. +4 −0 Appraisals
  3. +2 −2 Gemfile.lock
  4. +9 −118 README.md
  5. +1 −1  capybara-webkit.gemspec
  6. +1 −2  gemfiles/2.1.gemfile.lock
  7. +1 −1  gemfiles/2.2.gemfile.lock
  8. +1 −1  gemfiles/2.3.gemfile.lock
  9. +7 −0 gemfiles/2.4.gemfile
  10. +77 −0 gemfiles/2.4.gemfile.lock
  11. +38 −2 lib/capybara/webkit/browser.rb
  12. +15 −2 lib/capybara/webkit/connection.rb
  13. +74 −0 lib/capybara/webkit/driver.rb
  14. +3 −0  lib/capybara/webkit/errors.rb
  15. +274 −12 spec/driver_spec.rb
  16. +1 −1  spec/selenium_compatibility_spec.rb
  17. +1 −0  spec/spec_helper.rb
  18. +11 −0 src/AcceptAlert.cpp
  19. +10 −0 src/AcceptAlert.h
  20. +2 −0  src/CommandFactory.cpp
  21. +8 −3 src/Connection.cpp
  22. +1 −0  src/Connection.h
  23. +29 −0 src/FindModal.cpp
  24. +16 −0 src/FindModal.h
  25. +10 −2 src/SetConfirmAction.cpp
  26. +13 −2 src/SetPromptAction.cpp
  27. +100 −10 src/WebPage.cpp
  28. +12 −2 src/WebPage.h
  29. +9 −8 src/capybara.js
  30. +2 −0  src/find_command.h
  31. +4 −0 src/webkit_server.pro
View
8 .travis.yml
@@ -13,18 +13,18 @@ env:
matrix:
include:
- rvm: 1.9.3
- gemfile: gemfiles/2.1.gemfile
- env: QMAKE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake
- - rvm: 1.9.3
gemfile: gemfiles/2.2.gemfile
env: QMAKE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake
- rvm: 1.9.3
gemfile: gemfiles/2.3.gemfile
env: QMAKE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake
+ - rvm: 1.9.3
+ gemfile: gemfiles/2.4.gemfile
+ env: QMAKE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake
gemfile:
- - gemfiles/2.1.gemfile
- gemfiles/2.2.gemfile
- gemfiles/2.3.gemfile
+ - gemfiles/2.4.gemfile
before_install:
- sudo apt-add-repository -y ppa:ubuntu-sdk-team/ppa
- sudo apt-get update
View
4 Appraisals
@@ -9,3 +9,7 @@ end
appraise "2.3" do
gem "capybara", "~> 2.3.0"
end
+
+appraise "2.4" do
+ gem "capybara", "~> 2.4.0"
+end
View
4 Gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: .
specs:
capybara-webkit (1.2.0)
- capybara (>= 2.0.2, < 2.4.0)
+ capybara (>= 2.0.2, < 2.5.0)
json
GEM
@@ -12,7 +12,7 @@ GEM
appraisal (0.4.0)
bundler
rake
- capybara (2.3.0)
+ capybara (2.4.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
View
127 README.md
@@ -14,7 +14,7 @@ development toolkit. You'll need to download the Qt libraries to build and
install the gem. You can find instructions for downloading and installing QT on
the
[capybara-webkit wiki](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit).
-capybara-webkit requires Qt version 4.8.
+capybara-webkit requires Qt version 4.8 or greater.
Windows Support
---------------
@@ -47,7 +47,13 @@ CI
If you're like us, you'll be using capybara-webkit on CI.
-On Linux platforms, capybara-webkit requires an X server to run, although it doesn't create any visible windows. Xvfb works fine for this. You can setup Xvfb yourself and set a DISPLAY variable, or try out the [headless gem](https://github.com/leonid-shevtsov/headless).
+On Linux platforms, capybara-webkit requires an X server to run, although it doesn't create any visible windows. Xvfb works fine for this. You can setup Xvfb yourself and set a DISPLAY variable, try out the [headless gem](https://github.com/leonid-shevtsov/headless), or use the xvfb-run utility as follows:
+
+```
+xvfb-run -a bundle exec spec
+```
+
+This automatically sets up a virtual X server on a free server number.
Usage
-----
@@ -101,33 +107,6 @@ page.driver.error_messages
=> [{:source=>"http://example.com", :line_number=>1, :message=>"SyntaxError: Parse error"}]
```
-**alert_messages, confirm_messages, prompt_messages**: returns arrays of Javascript dialog messages for each dialog type
-
-```js
-// In Javascript:
-alert("HI");
-confirm("Ok?");
-prompt("Number?", "42");
-```
-
-```ruby
-# In Ruby:
-page.driver.alert_messages
-=> ["Hi"]
-page.driver.confirm_messages
-=> ["Ok?"]
-page.driver.prompt_messages
-=> ["Number?"]
-```
-
-**resize_window**: change the viewport size to the given width and height
-
-```ruby
-page.driver.resize_window(500, 300)
-page.driver.evaluate_script("window.innerWidth")
-=> 500
-```
-
**cookies**: allows read-only access of cookies for the current session
```ruby
@@ -135,94 +114,6 @@ page.driver.cookies["alpha"]
=> "abc"
```
-**accept_js_confirms!**: accept any Javascript confirm that is triggered by the page's Javascript
-
-```js
-// In Javascript:
-if (confirm("Ok?"))
- console.log("Hi");
-else
- console.log("Bye");
-```
-
-```ruby
-# In Ruby:
-page.driver.accept_js_confirms!
-visit "/"
-page.driver.console_messages.first[:message]
-=> "Hi"
-```
-
-**dismiss_js_confirms!**: dismiss any Javascript confirm that is triggered by the page's Javascript
-
-```js
-// In Javascript:
-if (confirm("Ok?"))
- console.log("Hi");
-else
- console.log("Bye");
-```
-
-```ruby
-# In Ruby:
-page.driver.dismiss_js_confirms!
-visit "/"
-page.driver.console_messages.first[:message]
-=> "Bye"
-```
-
-**accept_js_prompts!**: accept any Javascript prompt that is triggered by the page's Javascript
-
-```js
-// In Javascript:
-var a = prompt("Number?", "0")
-console.log(a);
-```
-
-```ruby
-# In Ruby:
-page.driver.accept_js_prompts!
-visit "/"
-page.driver.console_messages.first[:message]
-=> "0"
-```
-
-**dismiss_js_prompts!**: dismiss any Javascript prompt that is triggered by the page's Javascript
-
-```js
-// In Javascript:
-var a = prompt("Number?", "0")
-if (a != null)
- console.log(a);
-else
- console.log("you said no"));
-```
-
-```ruby
-# In Ruby:
-page.driver.dismiss_js_prompts!
-visit "/"
-page.driver.console_messages.first[:message]
-=> "you said no"
-```
-
-**js_prompt_input=(value)**: set the text to use if a Javascript prompt is encountered and accepted
-
-```js
-// In Javascript:
-var a = prompt("Number?", "0")
-console.log(a);
-```
-
-```ruby
-# In Ruby:
-page.driver.js_prompt_input = "42"
-page.driver.accept_js_prompts!
-visit "/"
-page.driver.console_messages.first[:message]
-=> "42"
-```
-
**header**: set the given HTTP header for subsequent requests
```ruby
@@ -248,4 +139,4 @@ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
License
-------
-capybara-webkit is Copyright (c) 2010-2013 thoughtbot, inc. It is free software, and may be redistributed under the terms specified in the LICENSE file.
+capybara-webkit is Copyright (c) 2010-2014 thoughtbot, inc. It is free software, and may be redistributed under the terms specified in the LICENSE file.
View
2  capybara-webkit.gemspec
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 1.9.0"
- s.add_runtime_dependency("capybara", ">= 2.0.2", "< 2.4.0")
+ s.add_runtime_dependency("capybara", ">= 2.0.2", "< 2.5.0")
s.add_runtime_dependency("json")
s.add_development_dependency("rspec", "~> 2.14.0")
View
3  gemfiles/2.1.gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../
specs:
capybara-webkit (1.2.0)
- capybara (>= 2.0.2, < 2.4.0)
+ capybara (>= 2.0.2, < 2.5.0)
json
GEM
@@ -24,7 +24,6 @@ GEM
ffi (1.9.3)
ffi (1.9.3-java)
json (1.8.1)
- json (1.8.1-java)
launchy (2.4.2)
addressable (~> 2.3)
launchy (2.4.2-java)
View
2  gemfiles/2.2.gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../
specs:
capybara-webkit (1.2.0)
- capybara (>= 2.0.2, < 2.4.0)
+ capybara (>= 2.0.2, < 2.5.0)
json
GEM
View
2  gemfiles/2.3.gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../
specs:
capybara-webkit (1.2.0)
- capybara (>= 2.0.2, < 2.4.0)
+ capybara (>= 2.0.2, < 2.5.0)
json
GEM
View
7 gemfiles/2.4.gemfile
@@ -0,0 +1,7 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "capybara", "~> 2.4.0"
+
+gemspec :path=>"../"
@mike-burns Owner

Missing newline terminator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
View
77 gemfiles/2.4.gemfile.lock
@@ -0,0 +1,77 @@
+PATH
+ remote: ../
+ specs:
+ capybara-webkit (1.2.0)
+ capybara (>= 2.0.2, < 2.5.0)
+ json
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.3.6)
+ appraisal (0.4.1)
+ bundler
+ rake
+ capybara (2.4.1)
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ childprocess (0.5.3)
+ ffi (~> 1.0, >= 1.0.11)
+ diff-lcs (1.2.5)
+ ffi (1.9.3)
+ json (1.8.1)
+ launchy (2.4.2)
+ addressable (~> 2.3)
+ mime-types (2.3)
+ mini_magick (3.7.0)
+ subexec (~> 0.2.1)
+ mini_portile (0.6.0)
+ multi_json (1.10.1)
+ nokogiri (1.6.2.1)
+ mini_portile (= 0.6.0)
+ rack (1.5.2)
+ rack-protection (1.5.3)
+ rack
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rake (10.3.2)
+ rspec (2.14.1)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ rspec-core (2.14.8)
+ rspec-expectations (2.14.5)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.14.6)
+ rubyzip (1.1.6)
+ selenium-webdriver (2.42.0)
+ childprocess (>= 0.5.0)
+ multi_json (~> 1.0)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0.4)
+ sinatra (1.4.5)
+ rack (~> 1.4)
+ rack-protection (~> 1.4)
+ tilt (~> 1.3, >= 1.3.4)
+ subexec (0.2.3)
+ tilt (1.4.1)
+ websocket (1.0.7)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ appraisal (~> 0.4.0)
+ capybara (~> 2.4.0)
+ capybara-webkit!
+ launchy
+ mini_magick
+ rake
+ rspec (~> 2.14.0)
+ selenium-webdriver
+ sinatra
View
40 lib/capybara/webkit/browser.rb
@@ -126,26 +126,54 @@ def get_window_handles
JSON.parse(command('GetWindowHandles'))
end
- alias_method :window_handles, :get_window_handles
+ def window_handles
+ warn '[DEPRECATION] Capybara::Webkit::Browser#window_handles ' \
+ 'is deprecated. Please use Capybara::Session#windows instead.'
+ get_window_handles
+ end
def get_window_handle
command('GetWindowHandle')
end
- alias_method :window_handle, :get_window_handle
+ def window_handle
+ warn '[DEPRECATION] Capybara::Webkit::Browser#window_handle ' \
+ 'is deprecated. Please use Capybara::Session#current_window instead.'
+ get_window_handle
+ end
+
+ def accept_confirm(options)
+ command("SetConfirmAction", "Yes", options[:text])
+ end
def accept_js_confirms
command("SetConfirmAction", "Yes")
end
+ def reject_confirm(options)
+ command("SetConfirmAction", "No", options[:text])
+ end
+
def reject_js_confirms
command("SetConfirmAction", "No")
end
+ def accept_prompt(options)
+ if options[:with]
+ command("SetPromptAction", "Yes", options[:text], options[:with])
+ else
+ command("SetPromptAction", "Yes", options[:text])
+ end
+ end
+
def accept_js_prompts
command("SetPromptAction", "Yes")
end
+ def reject_prompt(options)
+ command("SetPromptAction", "No", options[:text])
+ end
+
def reject_js_prompts
command("SetPromptAction", "No")
end
@@ -158,6 +186,14 @@ def clear_prompt_text
command("ClearPromptText")
end
+ def accept_alert(options)
+ command("AcceptAlert", options[:text])
+ end
+
+ def find_modal(id)
+ command("FindModal", id)
+ end
+
def url_blacklist=(black_list)
command("SetUrlBlacklist", *Array(black_list))
end
View
17 lib/capybara/webkit/connection.rb
@@ -35,11 +35,24 @@ def print(string)
end
def gets
- @socket.gets
+ response = ""
+ until response.match(/\n/) do
+ response += read(1)
+ end
+ response
end
def read(length)
- @socket.read(length)
+ response = ""
+ begin
+ while response.length < length do
+ response += @socket.read_nonblock(length - response.length)
+ end
+ rescue IO::WaitReadable
+ Thread.new { IO.select([@socket]) }.join
+ retry
+ end
+ response
end
private
View
74 lib/capybara/webkit/driver.rb
@@ -69,14 +69,22 @@ def error_messages
end
def alert_messages
+ warn '[DEPRECATION] Capybara::Webkit::Driver#alert_messages ' \
+ 'is deprecated. Please use Capybara::Session#accept_alert instead.'
browser.alert_messages
end
def confirm_messages
+ warn '[DEPRECATION] Capybara::Webkit::Driver#confirm_messages ' \
+ 'is deprecated. Please use Capybara::Session#accept_confirm ' \
+ 'or Capybara::Session#dismiss_confirm instead.'
browser.confirm_messages
end
def prompt_messages
+ warn '[DEPRECATION] Capybara::Webkit::Driver#prompt_messages ' \
+ 'is deprecated. Please use Capybara::Session#accept_prompt ' \
+ 'or Capybara::Session#dismiss_prompt instead.'
browser.prompt_messages
end
@@ -89,6 +97,8 @@ def status_code
end
def resize_window(width, height)
@jferris Owner
jferris added a note

This is part of the core API, so I don't think it should be deprecated: http://rubydoc.info/gems/capybara/Capybara/Driver/Base:resize_window_to

@jferris Owner
jferris added a note

Oh, no it's not; the core on is "resize_window_to." Please disregard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ warn '[DEPRECATION] Capybara::Webkit::Driver#resize_window ' \
+ 'is deprecated. Please use Capybara::Window#resize_to instead.'
resize_window_to(current_window_handle, width, height)
end
@@ -144,22 +154,32 @@ def maximize_window(selector)
end
def accept_js_confirms!
+ warn '[DEPRECATION] Capybara::Webkit::Driver#accept_js_confirms! ' \
+ 'is deprecated. Please use Capybara::Session#accept_confirm instead.'
browser.accept_js_confirms
end
def dismiss_js_confirms!
+ warn '[DEPRECATION] Capybara::Webkit::Driver#dismiss_js_confirms! ' \
+ 'is deprecated. Please use Capybara::Session#dismiss_confirm instead.'
browser.reject_js_confirms
end
def accept_js_prompts!
+ warn '[DEPRECATION] Capybara::Webkit::Driver#accept_js_prompts! ' \
+ 'is deprecated. Please use Capybara::Session#accept_prompt instead.'
browser.accept_js_prompts
end
def dismiss_js_prompts!
+ warn '[DEPRECATION] Capybara::Webkit::Driver#dismiss_js_prompts! ' \
+ 'is deprecated. Please use Capybara::Session#dismiss_prompt instead.'
browser.reject_js_prompts
end
def js_prompt_input=(value)
+ warn '[DEPRECATION] Capybara::Webkit::Driver#js_prompt_input= ' \
+ 'is deprecated. Please use Capybara::Session#accept_prompt instead.'
if value.nil?
browser.clear_prompt_text
else
@@ -175,6 +195,38 @@ def go_forward
browser.go_forward
end
+ def accept_modal(type, options={})
+ options = modal_action_options_for_browser(options)
+
+ case type
+ when :confirm
+ id = browser.accept_confirm(options)
+ when :prompt
+ id = browser.accept_prompt(options)
+ else
+ id = browser.accept_alert(options)
+ end
+
+ yield
+
+ find_modal(type, id, options)
+ end
+
+ def dismiss_modal(type, options={})
+ options = modal_action_options_for_browser(options)
+
+ case type
+ when :confirm
+ id = browser.reject_confirm(options)
+ else
+ id = browser.reject_prompt(options)
+ end
+
+ yield
+
+ find_modal(type, id, options)
+ end
+
def wait?
true
end
@@ -217,5 +269,27 @@ def version
browser.version
].join("\n")
end
+
+ private
+
+ def modal_action_options_for_browser(options)
+ if options[:text].is_a?(Regexp)
+ options.merge(text: options[:text].source)
+ else
+ options.merge(text: Regexp.escape(options[:text].to_s))
+ end.merge(original_text: options[:text])
+ end
+
+ def find_modal(type, id, options)
+ Timeout::timeout(options[:wait] || Capybara.default_wait_time) do
@jferris Owner
jferris added a note

It's odd that Capybara doesn't handle this, as it generally does with nodes and windows. Does Capybara expect find_modal to return the correct instance immediately?

find_modal isn't part of the Capybara API - because of the proactive/reactive nature of dealing with modals Capybara doesnt really provide anything for dealing with them, other than the external API definition

@mhoran Collaborator
mhoran added a note

Note that the window management APIs do not retry. If the title of the page does not match at the time of execution, window switching (or finding) will fail.

@mhorn window_opened_by retries until the new window is available (or timeout), and is now recommended as the preferred way of dealing with new windows opened during interaction

@mhoran Collaborator
mhoran added a note

Thanks for the clarification, @twalpole. I see that now -- I had only looked at within_window and switch_to_window.

@jferris, inspiration for find_modal came from the Selenium Driver implementation: https://github.com/jnicklas/capybara/blob/master/lib/capybara/selenium/driver.rb#L254. If we had a handle to a Document we could use synchronize as window_opened_by does. I'm not sure if that's possible, or desired.

@mhoran Collaborator
mhoran added a note

@jferris, I looked into making FindModal synchronous this evening but ran into an issue when the timeout is reached. The exception raised by Timeout::timeout results in a Reset being sent before FindModal has finished. Our Connection will start a command so long as the page is not loading -- which is desired in the case of Reset. However, this makes it possible for two commands to emit finished (and other signals), which results in undesired behavior.

I looked into stopping all pending commands when Reset is received, which fixes the issue. However, I'm not thrilled with what I came up with, and it's a bit of a rework that I'm not sure we should bundle in with Capybara 2.4 compatibility. However, I've definitely seen issues with simultaneous command processing in the past and it'd be a nice fix to have.

I'm happy to spend more time on this if you think it's worth getting synchronous FindCommand for this release, but if you're OK with polling for the interim I think that may be a safer bet while we come up with a polished solution.

@jferris Owner
jferris added a note

I think we should do what's reasonable to get Capybara 2.4 support out; it will be nice to get caught up.

After the release, though, I think it makes sense to try and make the server act synchronously with commands. I actually thought this was already the case, and I can't think of a reason you'd really want another command to go through before the previous command was finished.

@jferris Owner
jferris added a note

Looking at this a little more:

There's nothing on the C++ side which will prevent multiple commands being received and acted on, but the Ruby driver shouldn't ever do so. In browser.rb, it always waits for a response before returning from command, so unless the C++ command emits a response before it's actually finished, multiple commands can't execute simultaneously. Are you seeing behavior where that isn't the case?

@mhoran Collaborator
mhoran added a note

When an exception is raised in Ruby and a pending command has not finished (which would happen in the case of synchronous FindModal), Ruby will not wait for the command to finish before issuing a Reset. The reset will likely succeed, but if a subsequent test tries to use the driver, the outstanding command may still be running. Looking at the debug output, for example, you'll see two "started page load" messages printed, one for the outstanding command, and one for the subsequent command.

I think it's a good thing that Reset can be sent while a pending command is running, especially in the case of exceptions. However, we don't currently handle this very well and should probably make that code more robust. I tried this in https://github.com/mhoran/capybara-webkit/commits/synchronous_find_modal.

@mhoran Collaborator
mhoran added a note

I found a much easier way to ensure that we don't process two commands in the event of an exception/Reset cycle. However, in making FindModal synchronous I discovered a bug in JRuby: Timeout::timeout blocks when reading from a socket. This results in the timeout block executing, raising an exception after the block returns if the timeout is reached, but never timing out in the case that the underlying call blocks. This is an issue for synchronous FindCommand.

@mhoran Collaborator
mhoran added a note

cc6b910 resolves the JRuby timeout issue. I've pushed synchronous FindModal to this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ browser.find_modal(id)
+ end
+ rescue ModalNotFound
+ raise Capybara::ModalNotFound,
+ "Unable to find modal dialog#{" with #{options[:original_text]}" if options[:original_text]}"
+ rescue Timeout::Error
+ raise Capybara::ModalNotFound,
+ "Timed out waiting for modal dialog#{" with #{options[:original_text]}" if options[:original_text]}"
+ end
end
end
View
3  lib/capybara/webkit/errors.rb
@@ -20,6 +20,9 @@ class NoSuchWindowError < StandardError
class ConnectionError < StandardError
end
+ class ModalNotFound < StandardError
+ end
+
class JsonError
def initialize(response)
error = JSON.parse response
View
286 spec/driver_spec.rb
@@ -618,28 +618,101 @@ def visit(url, driver=driver)
end
context "javascript dialog interaction" do
+ before do
+ stub_const('Capybara::ModalNotFound', Class.new(StandardError))
+ end
+
context "on an alert app" do
let(:driver) do
- driver_for_html(<<-HTML)
- <html>
- <head>
- </head>
- <body>
- <script type="text/javascript">
- alert("Alert Text\\nGoes Here");
- </script>
- </body>
- </html>
- HTML
+ driver_for_app do
+ get '/' do
+ <<-HTML
+ <html>
+ <head>
+ </head>
+ <body>
+ <script type="text/javascript">
+ alert("Alert Text\\nGoes Here");
+ </script>
+ </body>
+ </html>
+ HTML
+ end
+
+ get '/async' do
+ <<-HTML
+ <html>
+ <head>
+ </head>
+ <body>
+ <script type="text/javascript">
+ function testAlert() {
+ setTimeout(function() { alert("Alert Text\\nGoes Here"); },
+ #{params[:sleep] || 100});
+ }
+ </script>
+ <input type="button" onclick="testAlert()" name="test"/>
+ </body>
+ </html>
+ HTML
+ end
+ end
end
- before { visit("/") }
+ it 'accepts any alert modal if no match is provided' do
+ alert_message = driver.accept_modal(:alert) do
+ visit("/")
+ end
+ alert_message.should eq "Alert Text\nGoes Here"
+ end
+
+ it 'accepts an alert modal if it matches' do
+ alert_message = driver.accept_modal(:alert, text: "Alert Text\nGoes Here") do
+ visit("/")
+ end
+ alert_message.should eq "Alert Text\nGoes Here"
+ end
+
+ it 'raises an error when accepting an alert modal that does not match' do
+ expect {
+ driver.accept_modal(:alert, text: 'No?') do
+ visit('/')
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
+ end
+
+ it 'waits to accept an async alert modal' do
+ visit("/async")
+ alert_message = driver.accept_modal(:alert) do
+ driver.find_xpath("//input").first.click
+ end
+ alert_message.should eq "Alert Text\nGoes Here"
+ end
+
+ it 'times out waiting for an async alert modal' do
+ visit("/async?sleep=1000")
+ expect {
+ driver.accept_modal(:alert, wait: 0.1) do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Timed out waiting for modal dialog"
+ end
+
+ it 'raises an error when an unexpected modal is displayed' do
+ expect {
+ driver.accept_modal(:confirm) do
+ visit("/")
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
+ end
it "should let me read my alert messages" do
+ visit("/")
driver.alert_messages.first.should eq "Alert Text\nGoes Here"
end
it "empties the array when reset" do
+ visit("/")
driver.reset!
driver.alert_messages.should be_empty
end
@@ -659,8 +732,25 @@ def visit(url, driver=driver)
else
console.log("goodbye");
}
+ function test_complex_dialog() {
+ if(confirm("Yes?"))
+ if(confirm("Really?"))
+ console.log("hello");
+ else
+ console.log("goodbye");
+ }
+ function test_async_dialog() {
+ setTimeout(function() {
+ if(confirm("Yes?"))
+ console.log("hello");
+ else
+ console.log("goodbye");
+ }, 100);
+ }
</script>
<input type="button" onclick="test_dialog()" name="test"/>
+ <input type="button" onclick="test_complex_dialog()" name="test_complex"/>
+ <input type="button" onclick="test_async_dialog()" name="test_async"/>
</body>
</html>
HTML
@@ -668,6 +758,81 @@ def visit(url, driver=driver)
before { visit("/") }
+ it 'accepts any confirm modal if no match is provided' do
+ driver.accept_modal(:confirm) do
+ driver.find_xpath("//input").first.click
+ end
+ driver.console_messages.first[:message].should eq "hello"
+ end
+
+ it 'dismisses a confirm modal that does not match' do
+ begin
+ driver.accept_modal(:confirm, text: 'No?') do
+ driver.find_xpath("//input").first.click
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+ rescue Capybara::ModalNotFound
+ end
+ end
+
+ it 'raises an error when accepting a confirm modal that does not match' do
+ expect {
+ driver.accept_modal(:confirm, text: 'No?') do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
+ end
+
+ it 'dismisses any confirm modal if no match is provided' do
+ driver.dismiss_modal(:confirm) do
+ driver.find_xpath("//input").first.click
+ end
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+
+ it 'raises an error when dismissing a confirm modal that does not match' do
+ expect {
+ driver.dismiss_modal(:confirm, text: 'No?') do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
+ end
+
+ it 'waits to accept an async confirm modal' do
+ visit("/async")
+ confirm_message = driver.accept_modal(:confirm) do
+ driver.find_css("input[name=test_async]").first.click
+ end
+ confirm_message.should eq "Yes?"
+ end
+
+ it 'allows the nesting of dismiss and accept' do
+ driver.dismiss_modal(:confirm) do
+ driver.accept_modal(:confirm) do
+ driver.find_css("input[name=test_complex]").first.click
+ end
+ end
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+
+ it 'raises an error when an unexpected modal is displayed' do
+ expect {
+ driver.accept_modal(:prompt) do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
+ end
+
+ it 'dismisses a confirm modal when prompt is expected' do
+ begin
+ driver.accept_modal(:prompt) do
+ driver.find_xpath("//input").first.click
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+ rescue Capybara::ModalNotFound
+ end
+ end
+
it "should default to accept the confirm" do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "hello"
@@ -727,8 +892,23 @@ def visit(url, driver=driver)
else
console.log("goodbye");
}
+ function test_complex_dialog() {
+ var response = prompt("Your name?", "John Smith");
+ if(response != null)
+ if(prompt("Your age?"))
+ console.log("hello " + response);
+ else
+ console.log("goodbye");
+ }
+ function test_async_dialog() {
+ setTimeout(function() {
+ var response = prompt("Your name?", "John Smith");
+ }, 100);
+ }
</script>
<input type="button" onclick="test_dialog()" name="test"/>
+ <input type="button" onclick="test_complex_dialog()" name="test_complex"/>
+ <input type="button" onclick="test_async_dialog()" name="test_async"/>
</body>
</html>
HTML
@@ -736,6 +916,88 @@ def visit(url, driver=driver)
before { visit("/") }
+ it 'accepts any prompt modal if no match is provided' do
+ driver.accept_modal(:prompt) do
+ driver.find_xpath("//input").first.click
+ end
+ driver.console_messages.first[:message].should eq "hello John Smith"
+ end
+
+ it 'accepts any prompt modal with the provided response' do
+ driver.accept_modal(:prompt, with: 'Capy') do
+ driver.find_xpath("//input").first.click
+ end
+ driver.console_messages.first[:message].should eq "hello Capy"
+ end
+
+ it 'raises an error when accepting a prompt modal that does not match' do
+ expect {
+ driver.accept_modal(:prompt, text: 'Your age?') do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with Your age?"
+ end
+
+ it 'dismisses any prompt modal if no match is provided' do
+ driver.dismiss_modal(:prompt) do
+ driver.find_xpath("//input").first.click
+ end
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+
+ it 'dismisses a prompt modal that does not match' do
+ begin
+ driver.accept_modal(:prompt, text: 'Your age?') do
+ driver.find_xpath("//input").first.click
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+ rescue Capybara::ModalNotFound
+ end
+ end
+
+ it 'raises an error when dismissing a prompt modal that does not match' do
+ expect {
+ driver.dismiss_modal(:prompt, text: 'Your age?') do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with Your age?"
+ end
+
+ it 'waits to accept an async prompt modal' do
+ visit("/async")
+ prompt_message = driver.accept_modal(:prompt) do
+ driver.find_css("input[name=test_async]").first.click
+ end
+ prompt_message.should eq "Your name?"
+ end
+
+ it 'allows the nesting of dismiss and accept' do
+ driver.dismiss_modal(:prompt) do
+ driver.accept_modal(:prompt) do
+ driver.find_css("input[name=test_complex]").first.click
+ end
+ end
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+
+ it 'raises an error when an unexpected modal is displayed' do
+ expect {
+ driver.accept_modal(:confirm) do
+ driver.find_xpath("//input").first.click
+ end
+ }.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
+ end
+
+ it 'dismisses a prompt modal when confirm is expected' do
+ begin
+ driver.accept_modal(:confirm) do
+ driver.find_xpath("//input").first.click
+ driver.console_messages.first[:message].should eq "goodbye"
+ end
+ rescue Capybara::ModalNotFound
+ end
+ end
+
it "should default to dismiss the prompt" do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"
View
2  spec/selenium_compatibility_spec.rb
@@ -3,7 +3,7 @@
describe Capybara::Webkit, 'compatibility with selenium' do
include AppRunner
- it 'generates the same events as selenium when filling out forms' do
+ it 'generates the same events as selenium when filling out forms', selenium_compatibility: true do
run_application_for_html(<<-HTML)
<html><body>
<form onsubmit="return false">
View
1  spec/spec_helper.rb
@@ -28,6 +28,7 @@
c.filter_run_excluding :skip_on_windows => !(RbConfig::CONFIG['host_os'] =~ /mingw32/).nil?
c.filter_run_excluding :skip_on_jruby => !defined?(::JRUBY_VERSION).nil?
+ c.filter_run_excluding :selenium_compatibility => (Capybara::VERSION =~ /^2\.4\./).nil?
# We can't support outerWidth and outerHeight without a visible window.
# We focus the next window instead of failing when closing windows.
View
11 src/AcceptAlert.cpp
@@ -0,0 +1,11 @@
+#include "AcceptAlert.h"
+#include "SocketCommand.h"
+#include "WebPage.h"
+#include "WebPageManager.h"
+
+AcceptAlert::AcceptAlert(WebPageManager *manager, QStringList &arguments, QObject *parent) : SocketCommand(manager, arguments, parent) {
+}
+
+void AcceptAlert::start() {
+ finish(true, page()->acceptAlert(arguments()[0]));
+}
View
10 src/AcceptAlert.h
@@ -0,0 +1,10 @@
+#include "SocketCommand.h"
+
+class AcceptAlert : public SocketCommand {
+ Q_OBJECT
+
+ public:
+ AcceptAlert(WebPageManager *, QStringList &arguments, QObject *parent = 0);
+ virtual void start();
+};
+
View
2  src/CommandFactory.cpp
@@ -46,6 +46,8 @@
#include "WindowMaximize.h"
#include "GoBack.h"
#include "GoForward.h"
+#include "AcceptAlert.h"
+#include "FindModal.h"
CommandFactory::CommandFactory(WebPageManager *manager, QObject *parent) : QObject(parent) {
m_manager = manager;
View
11 src/Connection.cpp
@@ -17,6 +17,7 @@ Connection::Connection(QTcpSocket *socket, WebPageManager *manager, QObject *par
m_commandFactory = new CommandFactory(m_manager, this);
m_commandParser = new CommandParser(socket, m_commandFactory, this);
m_pageSuccess = true;
+ m_pendingCommand = NULL;
connect(m_socket, SIGNAL(readyRead()), m_commandParser, SLOT(checkNext()));
connect(m_commandParser, SIGNAL(commandReady(Command *)), this, SLOT(commandReady(Command *)));
connect(m_manager, SIGNAL(pageFinished(bool)), this, SLOT(pendingLoadFinished(bool)));
@@ -28,10 +29,13 @@ void Connection::commandReady(Command *command) {
}
void Connection::startCommand(Command *command) {
+ if (m_pendingCommand) {
+ m_pendingCommand->deleteLater();
+ }
if (m_pageSuccess) {
- command = new TimeoutCommand(new PageLoadingCommand(command, m_manager, this), m_manager, this);
- connect(command, SIGNAL(finished(Response *)), this, SLOT(finishCommand(Response *)));
- command->start();
+ m_pendingCommand = new TimeoutCommand(new PageLoadingCommand(command, m_manager, this), m_manager, this);
+ connect(m_pendingCommand, SIGNAL(finished(Response *)), this, SLOT(finishCommand(Response *)));
+ m_pendingCommand->start();
} else {
writePageLoadFailure();
}
@@ -52,6 +56,7 @@ void Connection::finishCommand(Response *response) {
m_pageSuccess = true;
writeResponse(response);
sender()->deleteLater();
+ m_pendingCommand = NULL;
}
void Connection::writeResponse(Response *response) {
View
1  src/Connection.h
@@ -32,5 +32,6 @@ class Connection : public QObject {
CommandFactory *m_commandFactory;
bool m_pageSuccess;
WebPage *currentPage();
+ Command *m_pendingCommand;
};
View
29 src/FindModal.cpp
@@ -0,0 +1,29 @@
+#include "FindModal.h"
+#include "SocketCommand.h"
+#include "WebPage.h"
+#include "WebPageManager.h"
+#include "ErrorMessage.h"
+
+FindModal::FindModal(WebPageManager *manager, QStringList &arguments, QObject *parent) : SocketCommand(manager, arguments, parent) {
+ m_modalId = 0;
+}
+
+void FindModal::start() {
+ m_modalId = arguments()[0].toInt();
+ if (page()->modalCount() < m_modalId) {
+ connect(page(), SIGNAL(modalReady(int)), SLOT(handleModalReady(int)));
+ } else {
+ handleModalReady(m_modalId);
+ }
+}
+
+void FindModal::handleModalReady(int modalId) {
+ if (modalId == m_modalId) {
+ sender()->disconnect(SIGNAL(modalReady(int)), this, SLOT(handleModalReady(int)));
+ if (page()->modalMessage(m_modalId).isNull()) {
+ finish(false, new ErrorMessage("ModalNotFound", ""));
+ } else {
+ finish(true, page()->modalMessage(m_modalId));
+ }
+ }
+}
View
16 src/FindModal.h
@@ -0,0 +1,16 @@
+#include "SocketCommand.h"
+
+class FindModal : public SocketCommand {
+ Q_OBJECT
+
+ public:
+ FindModal(WebPageManager *, QStringList &arguments, QObject *parent = 0);
+ virtual void start();
+
+ public slots:
+ void handleModalReady(int);
+
+ private:
+ int m_modalId;
+};
+
View
12 src/SetConfirmAction.cpp
@@ -6,6 +6,14 @@ SetConfirmAction::SetConfirmAction(WebPageManager *manager, QStringList &argumen
void SetConfirmAction::start()
{
- page()->setConfirmAction(arguments()[0]);
- finish(true);
+ QString index;
+ switch (arguments().length()) {
+ case 2:
+ index = page()->setConfirmAction(arguments()[0], arguments()[1]);
+ break;
+ default:
+ page()->setConfirmAction(arguments()[0]);
+ }
+
+ finish(true, index);
}
View
15 src/SetPromptAction.cpp
@@ -6,6 +6,17 @@ SetPromptAction::SetPromptAction(WebPageManager *manager, QStringList &arguments
void SetPromptAction::start()
{
- page()->setPromptAction(arguments()[0]);
- finish(true);
+ QString index;
+ switch (arguments().length()) {
+ case 3:
+ index = page()->setPromptAction(arguments()[0], arguments()[1], arguments()[2]);
+ break;
+ case 2:
+ index = page()->setPromptAction(arguments()[0], arguments()[1]);
+ break;
+ default:
+ page()->setPromptAction(arguments()[0]);
+ }
+
+ finish(true, index);
}
View
110 src/WebPage.cpp
@@ -19,14 +19,13 @@ WebPage::WebPage(WebPageManager *manager, QObject *parent) : QWebPage(parent) {
m_failed = false;
m_manager = manager;
m_uuid = QUuid::createUuid().toString();
+ m_confirmAction = true;
+ m_promptAction = false;
setForwardUnsupportedContent(true);
loadJavascript();
setUserStylesheet();
- m_confirm = true;
- m_prompt = false;
- m_prompt_text = QString();
this->setCustomNetworkAccessManager();
connect(this, SIGNAL(loadStarted()), this, SLOT(loadStarted()));
@@ -180,26 +179,71 @@ void WebPage::javaScriptConsoleMessage(const QString &message, int lineNumber, c
void WebPage::javaScriptAlert(QWebFrame *frame, const QString &message) {
Q_UNUSED(frame);
m_alertMessages.append(message);
+
+ if (m_modalResponses.isEmpty()) {
+ m_modalMessages << QString();
+ } else {
+ QVariantMap alertResponse = m_modalResponses.takeLast();
+ bool expectedType = alertResponse["type"].toString() == "alert";
+ QRegExp expectedMessage = alertResponse["message"].toRegExp();
+
+ addModalMessage(expectedType, message, expectedMessage);
+ }
+
m_manager->logger() << "ALERT:" << qPrintable(message);
}
bool WebPage::javaScriptConfirm(QWebFrame *frame, const QString &message) {
Q_UNUSED(frame);
m_confirmMessages.append(message);
- return m_confirm;
+
+ if (m_modalResponses.isEmpty()) {
+ m_modalMessages << QString();
+ return m_confirmAction;
+ } else {
+ QVariantMap confirmResponse = m_modalResponses.takeLast();
+ bool expectedType = confirmResponse["type"].toString() == "confirm";
+ QRegExp expectedMessage = confirmResponse["message"].toRegExp();
+
+ addModalMessage(expectedType, message, expectedMessage);
+ return expectedType &&
+ confirmResponse["action"].toBool() &&
+ message.contains(expectedMessage);
+ }
}
bool WebPage::javaScriptPrompt(QWebFrame *frame, const QString &message, const QString &defaultValue, QString *result) {
Q_UNUSED(frame)
m_promptMessages.append(message);
- if (m_prompt) {
- if (m_prompt_text.isNull()) {
+
+ bool action = false;
+ QString response;
+
+ if (m_modalResponses.isEmpty()) {
+ action = m_promptAction;
+ response = m_prompt_text;
+ m_modalMessages << QString();
+ } else {
+ QVariantMap promptResponse = m_modalResponses.takeLast();
+ bool expectedType = promptResponse["type"].toString() == "prompt";
+ QRegExp expectedMessage = promptResponse["message"].toRegExp();
+
+ action = expectedType &&
+ promptResponse["action"].toBool() &&
+ message.contains(expectedMessage);
+ response = promptResponse["response"].toString();
+ addModalMessage(expectedType, message, expectedMessage);
+ }
+
+ if (action) {
+ if (response.isNull()) {
*result = defaultValue;
} else {
- *result = m_prompt_text;
+ *result = response;
}
}
- return m_prompt;
+
+ return action;
}
void WebPage::loadStarted() {
@@ -376,15 +420,61 @@ void WebPage::remove() {
m_manager->removePage(this);
}
+QString WebPage::setConfirmAction(QString action, QString message) {
+ QVariantMap confirmResponse;
+ confirmResponse["type"] = "confirm";
+ confirmResponse["action"] = (action=="Yes");
+ confirmResponse["message"] = QRegExp(message);
+ m_modalResponses << confirmResponse;
+ return QString::number(m_modalResponses.length());
+}
+
void WebPage::setConfirmAction(QString action) {
- m_confirm = (action == "Yes");
+ m_confirmAction = (action == "Yes");
+}
+
+QString WebPage::setPromptAction(QString action, QString message, QString response) {
+ QVariantMap promptResponse;
+ promptResponse["type"] = "prompt";
+ promptResponse["action"] = (action == "Yes");
+ promptResponse["message"] = QRegExp(message);
+ promptResponse["response"] = response;
+ m_modalResponses << promptResponse;
+ return QString::number(m_modalResponses.length());
+}
+
+QString WebPage::setPromptAction(QString action, QString message) {
+ return setPromptAction(action, message, QString());
}
void WebPage::setPromptAction(QString action) {
- m_prompt = (action == "Yes");
+ m_promptAction = (action == "Yes");
}
void WebPage::setPromptText(QString text) {
m_prompt_text = text;
}
+QString WebPage::acceptAlert(QString message) {
+ QVariantMap alertResponse;
+ alertResponse["type"] = "alert";
+ alertResponse["message"] = QRegExp(message);
+ m_modalResponses << alertResponse;
+ return QString::number(m_modalResponses.length());
+}
+
+int WebPage::modalCount() {
+ return m_modalMessages.length();
+}
+
+QString WebPage::modalMessage(int id) {
+ return m_modalMessages[id - 1];
+}
+
+void WebPage::addModalMessage(bool expectedType, const QString &message, const QRegExp &expectedMessage) {
+ if (expectedType && message.contains(expectedMessage))
+ m_modalMessages << message;
+ else
+ m_modalMessages << QString();
+ emit modalReady(m_modalMessages.length());
+}
View
14 src/WebPage.h
@@ -23,8 +23,12 @@ class WebPage : public QWebPage {
QString userAgentForUrl(const QUrl &url ) const;
void setUserAgent(QString userAgent);
void setConfirmAction(QString action);
+ QString setConfirmAction(QString action, QString message);
+ QString setPromptAction(QString action, QString message, QString response);
+ QString setPromptAction(QString action, QString message);
void setPromptAction(QString action);
void setPromptText(QString action);
+ QString acceptAlert(QString);
int getLastStatus();
void setCustomNetworkAccessManager();
bool render(const QString &fileName, const QSize &minimumSize);
@@ -48,6 +52,8 @@ class WebPage : public QWebPage {
void mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button);
bool clickTest(QWebElement element, int absoluteX, int absoluteY);
void resize(int, int);
+ int modalCount();
+ QString modalMessage(int);
public slots:
bool shouldInterruptJavaScript();
@@ -65,6 +71,7 @@ class WebPage : public QWebPage {
void pageFinished(bool);
void requestCreated(QByteArray &url, QNetworkReply *reply);
void replyFinished(QNetworkReply *reply);
+ void modalReady(int);
protected:
virtual void javaScriptConsoleMessage(const QString &message, int lineNumber, const QString &sourceID);
@@ -82,8 +89,8 @@ class WebPage : public QWebPage {
QStringList getAttachedFileNames();
void loadJavascript();
void setUserStylesheet();
- bool m_confirm;
- bool m_prompt;
+ bool m_confirmAction;
+ bool m_promptAction;
QVariantList m_consoleMessages;
QVariantList m_alertMessages;
QVariantList m_confirmMessages;
@@ -94,6 +101,9 @@ class WebPage : public QWebPage {
QString m_errorPageMessage;
void setFrameProperties(QWebFrame *, QUrl &, NetworkReplyProxy *);
QPoint m_mousePosition;
+ QList<QVariantMap> m_modalResponses;
+ QStringList m_modalMessages;
+ void addModalMessage(bool, const QString &, const QRegExp &);
};
#endif //_WEBPAGE_H
View
17 src/capybara.js
@@ -272,8 +272,6 @@ Capybara = {
textTypes = ["email", "number", "password", "search", "tel", "text", "textarea", "url"];
if (textTypes.indexOf(type) != -1) {
- this.focus(index);
-
maxLength = this.attribute(index, "maxlength");
if (maxLength && value.length > maxLength) {
length = maxLength;
@@ -281,15 +279,18 @@ Capybara = {
length = value.length;
}
- if (!node.readOnly)
+ if (!node.readOnly) {
+ this.focus(index);
+
node.value = "";
- for (strindex = 0; strindex < length; strindex++) {
- CapybaraInvocation.keypress(value[strindex]);
- }
+ for (strindex = 0; strindex < length; strindex++) {
+ CapybaraInvocation.keypress(value[strindex]);
+ }
- if (value == '')
- this.trigger(index, "change");
+ if (value == '')
+ this.trigger(index, "change");
+ }
} else if (type === "checkbox" || type === "radio") {
if (node.checked != (value === "true")) {
this.leftClick(index);
View
2  src/find_command.h
@@ -48,3 +48,5 @@ CHECK_COMMAND(WindowSize)
CHECK_COMMAND(WindowMaximize)
CHECK_COMMAND(GoBack)
CHECK_COMMAND(GoForward)
+CHECK_COMMAND(AcceptAlert)
+CHECK_COMMAND(FindModal)
View
4 src/webkit_server.pro
@@ -7,6 +7,8 @@ PRECOMPILED_DIR = $${BUILD_DIR}
OBJECTS_DIR = $${BUILD_DIR}
MOC_DIR = $${BUILD_DIR}
HEADERS = \
+ FindModal.h \
+ AcceptAlert.h \
GoForward.h \
GoBack.h \
WindowMaximize.h \
@@ -79,6 +81,8 @@ HEADERS = \
StdinNotifier.h
SOURCES = \
+ FindModal.cpp \
+ AcceptAlert.cpp \
GoForward.cpp \
GoBack.cpp \
WindowMaximize.cpp \
Something went wrong with that request. Please try again.