Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Extend `Node#set` with the `datetime-local` input type [#295]
- Support HTML5 drag-and-drop and drag modifier keys in `Node#drag_to` [#315]
- Support `Element#drop` for files and strings [#316]
- Add `:raise_on_unhandled_modal` browser option to raise instead of warn on an unhandled modal [#320]

### Changed
- Bump Ferrum dependency to `~> 0.18.0`
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ end
`Cuprite`-specific options are:

* options `Hash`
* `:raise_on_unhandled_modal` (Boolean) - When set to `false`, output a warning. When set to `true`, raise
`Capybara::Cuprite::UnhandledModalError` instead. The dialog is always auto-accepted either way; the raise is
deferred and surfaces on the next command sent to the browser. In practice that's almost always the same action
that triggered the dialog (a click, `visit`, `evaluate_script`, ...), since a JS dialog blocks the page until
answered, so that action's own command is what was waiting. A dialog fired with nothing in flight (e.g. a bare JS
timer) only surfaces on whatever command runs next.
* `:url_blacklist` (Array) - array of regexes to match against requested URLs
* `:url_whitelist` (Array) - array of regexes to match against requested URLs

Expand Down
9 changes: 9 additions & 0 deletions lib/capybara/cuprite/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Browser < Ferrum::Browser
def initialize(options = nil)
super

@options.raise_on_unhandled_modal = options&.dig(:raise_on_unhandled_modal)
@options.url_blacklist = prepare_wildcards(options&.dig(:url_blacklist))
@options.url_whitelist = prepare_wildcards(options&.dig(:url_whitelist))

Expand Down Expand Up @@ -51,6 +52,14 @@ def resize(**options)
super
end

def raise_on_unhandled_modal
@options.raise_on_unhandled_modal
end

def raise_on_unhandled_modal=(value)
@options.raise_on_unhandled_modal = value
end

def url_whitelist
@options.url_whitelist
end
Expand Down
1 change: 1 addition & 0 deletions lib/capybara/cuprite/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def reset!
@paper_size = nil
browser.url_blacklist = @options[:url_blacklist]
browser.url_whitelist = @options[:url_whitelist]
browser.raise_on_unhandled_modal = @options.fetch(:raise_on_unhandled_modal, false)
browser.reset
@started = false
end
Expand Down
9 changes: 9 additions & 0 deletions lib/capybara/cuprite/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ def message
end
end

class UnhandledModalError < Error
attr_reader :message

def initialize(message)
@message = message
super()
end
end

class ObsoleteNode < ClientError
attr_reader :node

Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/cuprite/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Ferrum
class Browser
class Options
attr_writer :window_size
attr_accessor :url_blacklist, :url_whitelist
attr_accessor :url_blacklist, :url_whitelist, :raise_on_unhandled_modal

def reset_window_size
@window_size = @options[:window_size]
Expand Down
50 changes: 45 additions & 5 deletions lib/capybara/cuprite/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,25 @@ def initialize(...)
@accept_modal = []
@modal_messages = []
@modal_response = nil
@unhandled_modal_error = nil
super
end

# Keep a handle to Ferrum's own implementation before overriding it
# below, so answering a dialog (see `handle_javascript_dialog`) can
# bypass our override.
alias ferrum_command command

# The `Page.javascriptDialogOpening` event is handled on Ferrum's
# background CDP dispatcher thread, so raising there wouldn't reach
# the caller and would permanently kill that thread instead. The
# dialog is always accepted immediately from that thread, and the
# error (if any) is stashed here to be raised from the main thread
# the next time it makes a command round trip.
def command(...)
raise_pending_unhandled_modal_error! { super }
end

def set(node, value)
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
Expand Down Expand Up @@ -100,6 +116,7 @@ def reset_modals
@accept_modal = []
@modal_response = nil
@modal_messages = []
@unhandled_modal_error = nil
end

def before_click(node, name, _keys = [], offset = {})
Expand Down Expand Up @@ -141,6 +158,18 @@ def closed?

private

def raise_pending_unhandled_modal_error!
error = @unhandled_modal_error
@unhandled_modal_error = nil
raise error if error

yield
ensure
error = @unhandled_modal_error
@unhandled_modal_error = nil
raise error if error
end

def prepare_page
super

Expand All @@ -155,22 +184,33 @@ def prepare_page

on("Page.javascriptDialogOpening") do |params|
accept_modal = @accept_modal.last
unhandled_modal_error = nil

if [true, false].include?(accept_modal)
@accept_modal.pop
@modal_messages << params["message"]
options = { accept: accept_modal }
response = @modal_response || params["defaultPrompt"]
else
with_text = params["message"] ? "with text `#{params['message']}` " : ""
warn "Modal window #{with_text}has been opened, but you didn't wrap " \
"your code into (`accept_prompt` | `dismiss_prompt` | " \
"`accept_confirm` | `dismiss_confirm` | `accept_alert`), " \
"accepting by default"
message = "Modal window #{with_text}has been opened, but you didn't wrap " \
"your code into (`accept_prompt` | `dismiss_prompt` | " \
"`accept_confirm` | `dismiss_confirm` | `accept_alert`), " \
"accepting by default"

if @options.raise_on_unhandled_modal
unhandled_modal_error = UnhandledModalError.new(message)
else
warn message
end

options = { accept: true }
response = params["defaultPrompt"]
end
options.merge!(promptText: response) if response
command("Page.handleJavaScriptDialog", **options)
ferrum_command("Page.handleJavaScriptDialog", **options)

@unhandled_modal_error = unhandled_modal_error if unhandled_modal_error
end
end

Expand Down
13 changes: 13 additions & 0 deletions spec/features/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,19 @@
expect(@session).to have_xpath("//a[@id='open-match' and @confirmed='true']")
end

it "configured to raise instead of warning" do
@session.driver.browser.raise_on_unhandled_modal = true

@session.visit "/cuprite/with_js"

expect { @session.click_link("Open for match") }.to raise_error(
Capybara::Cuprite::UnhandledModalError,
"Modal window with text `{T}ext \\w|th [reg.exp] (chara©+er$)?` has been opened, " \
"but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` " \
"| `dismiss_confirm` | `accept_alert`), accepting by default"
)
end

it "matches on partial strings" do
@session.visit "/cuprite/with_js"
expect do
Expand Down
11 changes: 11 additions & 0 deletions spec/lib/driver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
end
end

describe "raise_on_unhandled_modal configuration" do
it "survives resetting the driver between examples" do
driver = described_class.new(nil, { raise_on_unhandled_modal: true })

driver.browser

expect { driver.reset! }.not_to(change { driver.browser.raise_on_unhandled_modal })
expect(driver.browser.raise_on_unhandled_modal).to eq(true)
end
end

describe "save_path configuration" do
it "defaults to the Capybara save path" do
driver = with_capybara_save_path("/tmp/capybara-save-path") do
Expand Down
Loading