From 907c9ec25c5a0638bdf6dad59da047e8bed08bcc Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Wed, 18 Jul 2018 19:06:05 +0300 Subject: [PATCH] Initial commit --- .gitignore | 1 + Gemfile | 7 + Gemfile.lock | 91 + Rakefile | 9 + bin/console | 8 + config.ru | 5 + cuprite.gemspec | 31 + lib/capybara/cuprite.rb | 25 + lib/capybara/cuprite/browser.rb | 401 +++++ lib/capybara/cuprite/browser/client.rb | 55 + lib/capybara/cuprite/browser/process.rb | 134 ++ lib/capybara/cuprite/browser/web_socket.rb | 80 + lib/capybara/cuprite/cookie.rb | 41 + lib/capybara/cuprite/driver.rb | 397 +++++ lib/capybara/cuprite/errors.rb | 177 ++ lib/capybara/cuprite/node.rb | 206 +++ lib/capybara/cuprite/version.rb | 7 + spec/integration/driver_spec.rb | 1547 +++++++++++++++++ spec/integration/session_spec.rb | 1071 ++++++++++++ spec/spec_helper.rb | 83 + spec/support/custom_chrome | 3 + spec/support/geolocation.js | 7 + spec/support/public/jquery-1.11.3.min.js | 5 + spec/support/public/jquery-ui-1.11.4.min.js | 13 + spec/support/public/test.js | 64 + spec/support/spec_logger.rb | 15 + spec/support/test_app.rb | 98 ++ spec/support/views/attach_file.erb | 10 + spec/support/views/attributes_properties.erb | 10 + spec/support/views/basic_auth.erb | 5 + spec/support/views/click_coordinates.erb | 30 + spec/support/views/click_test.erb | 52 + spec/support/views/console_log.erb | 12 + spec/support/views/date_fields.erb | 9 + spec/support/views/datepicker.erb | 15 + spec/support/views/double_click_test.erb | 48 + spec/support/views/drag.erb | 59 + spec/support/views/filter_text_test.erb | 9 + spec/support/views/fixed_positioning.erb | 6 + spec/support/views/frames.erb | 13 + spec/support/views/headers.erb | 3 + spec/support/views/headers_with_ajax.erb | 25 + spec/support/views/image_map.erb | 32 + spec/support/views/index.erb | 6 + spec/support/views/js_error.erb | 5 + spec/support/views/js_redirect.erb | 8 + spec/support/views/long_page.erb | 41 + spec/support/views/nested_frame_test.erb | 13 + spec/support/views/popup_headers.erb | 1 + .../views/requiring_custom_extension.erb | 14 + spec/support/views/scroll.erb | 22 + spec/support/views/send_keys.erb | 35 + spec/support/views/set.erb | 10 + spec/support/views/simple.erb | 15 + spec/support/views/svg_test.erb | 12 + spec/support/views/table.erb | 7 + spec/support/views/unwanted.erb | 6 + spec/support/views/url_blacklist.erb | 9 + spec/support/views/url_whitelist.erb | 10 + spec/support/views/visible.erb | 19 + spec/support/views/visit_timeout.erb | 7 + spec/support/views/wanted.erb | 6 + spec/support/views/with_ajax_fail.erb | 16 + .../views/with_different_resources.erb | 15 + spec/support/views/with_js.erb | 75 + spec/support/views/zoom_test.erb | 6 + spec/tmp/.gitkeep | 0 spec/tmp/a_test_pathname | 1 + spec/tmp/screenshot.jpeg | Bin 0 -> 29280 bytes spec/unit/browser_spec.rb | 24 + spec/unit/client_spec.rb | 27 + spec/unit/driver_spec.rb | 45 + 72 files changed, 5374 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100755 bin/console create mode 100644 config.ru create mode 100644 cuprite.gemspec create mode 100644 lib/capybara/cuprite.rb create mode 100644 lib/capybara/cuprite/browser.rb create mode 100644 lib/capybara/cuprite/browser/client.rb create mode 100644 lib/capybara/cuprite/browser/process.rb create mode 100644 lib/capybara/cuprite/browser/web_socket.rb create mode 100644 lib/capybara/cuprite/cookie.rb create mode 100644 lib/capybara/cuprite/driver.rb create mode 100644 lib/capybara/cuprite/errors.rb create mode 100644 lib/capybara/cuprite/node.rb create mode 100644 lib/capybara/cuprite/version.rb create mode 100644 spec/integration/driver_spec.rb create mode 100644 spec/integration/session_spec.rb create mode 100644 spec/spec_helper.rb create mode 100755 spec/support/custom_chrome create mode 100644 spec/support/geolocation.js create mode 100644 spec/support/public/jquery-1.11.3.min.js create mode 100644 spec/support/public/jquery-ui-1.11.4.min.js create mode 100644 spec/support/public/test.js create mode 100644 spec/support/spec_logger.rb create mode 100644 spec/support/test_app.rb create mode 100644 spec/support/views/attach_file.erb create mode 100644 spec/support/views/attributes_properties.erb create mode 100644 spec/support/views/basic_auth.erb create mode 100644 spec/support/views/click_coordinates.erb create mode 100644 spec/support/views/click_test.erb create mode 100644 spec/support/views/console_log.erb create mode 100644 spec/support/views/date_fields.erb create mode 100644 spec/support/views/datepicker.erb create mode 100644 spec/support/views/double_click_test.erb create mode 100644 spec/support/views/drag.erb create mode 100644 spec/support/views/filter_text_test.erb create mode 100644 spec/support/views/fixed_positioning.erb create mode 100644 spec/support/views/frames.erb create mode 100644 spec/support/views/headers.erb create mode 100644 spec/support/views/headers_with_ajax.erb create mode 100644 spec/support/views/image_map.erb create mode 100644 spec/support/views/index.erb create mode 100644 spec/support/views/js_error.erb create mode 100644 spec/support/views/js_redirect.erb create mode 100644 spec/support/views/long_page.erb create mode 100644 spec/support/views/nested_frame_test.erb create mode 100644 spec/support/views/popup_headers.erb create mode 100644 spec/support/views/requiring_custom_extension.erb create mode 100644 spec/support/views/scroll.erb create mode 100644 spec/support/views/send_keys.erb create mode 100644 spec/support/views/set.erb create mode 100644 spec/support/views/simple.erb create mode 100644 spec/support/views/svg_test.erb create mode 100644 spec/support/views/table.erb create mode 100644 spec/support/views/unwanted.erb create mode 100644 spec/support/views/url_blacklist.erb create mode 100644 spec/support/views/url_whitelist.erb create mode 100644 spec/support/views/visible.erb create mode 100644 spec/support/views/visit_timeout.erb create mode 100644 spec/support/views/wanted.erb create mode 100644 spec/support/views/with_ajax_fail.erb create mode 100644 spec/support/views/with_different_resources.erb create mode 100644 spec/support/views/with_js.erb create mode 100644 spec/support/views/zoom_test.erb create mode 100644 spec/tmp/.gitkeep create mode 100644 spec/tmp/a_test_pathname create mode 100644 spec/tmp/screenshot.jpeg create mode 100644 spec/unit/browser_spec.rb create mode 100644 spec/unit/client_spec.rb create mode 100644 spec/unit/driver_spec.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36298d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.byebug_history diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..bbcf2d1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "puma" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b42ac8a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,91 @@ +PATH + remote: . + specs: + cuprite (0.1.0) + capybara (>= 2.1, < 4) + cliver (~> 0.3) + websocket-driver (~> 0.7) + +GEM + remote: https://rubygems.org/ + specs: + Ascii85 (1.0.3) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + afm (0.2.2) + byebug (10.0.2) + capybara (3.3.1) + addressable + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + xpath (~> 3.1) + cliver (0.3.2) + diff-lcs (1.3) + hashery (2.1.2) + image_size (2.0.0) + launchy (2.4.3) + addressable (~> 2.3) + mini_mime (1.0.0) + mini_portile2 (2.3.0) + mustermann (1.0.2) + nokogiri (1.8.4) + mini_portile2 (~> 2.3.0) + pdf-reader (2.1.0) + Ascii85 (~> 1.0.0) + afm (~> 0.2.1) + hashery (~> 2.0) + ruby-rc4 + ttfunk + public_suffix (3.0.2) + puma (3.11.4) + rack (2.0.5) + rack-protection (2.0.3) + rack + rack-test (1.0.0) + rack (>= 1.0, < 3) + rake (12.3.1) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + ruby-rc4 (0.1.5) + sinatra (2.0.3) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.3) + tilt (~> 2.0) + tilt (2.0.8) + ttfunk (1.5.1) + websocket-driver (0.7.0) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + xpath (3.1.0) + nokogiri (~> 1.8) + +PLATFORMS + ruby + +DEPENDENCIES + byebug (~> 10.0) + cuprite! + image_size (~> 2.0) + launchy (~> 2.4) + pdf-reader (~> 2.1) + puma + rake (~> 12.3) + rspec (~> 3.7) + sinatra (~> 2.0) + +BUNDLED WITH + 1.16.2 diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..a26d372 --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "bundler/setup" +require "rspec/core/rake_task" + +require "capybara/cuprite/version" + +RSpec::Core::RakeTask.new("test") +task default: :test diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..827a9bc --- /dev/null +++ b/bin/console @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby + +require "irb" +require "irb/completion" + +require "cuprite" + +IRB.start diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..f194e54 --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "bundler/setup" +require File.expand_path("spec/support/test_app") +run TestApp diff --git a/cuprite.gemspec b/cuprite.gemspec new file mode 100644 index 0000000..124f0df --- /dev/null +++ b/cuprite.gemspec @@ -0,0 +1,31 @@ +lib = File.expand_path("lib", __dir__) +$:.unshift lib unless $:.include?(lib) + +require "capybara/cuprite/version" + +Gem::Specification.new do |s| + s.name = "cuprite" + s.version = Capybara::Cuprite::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ["Dmitry Vorotilin"] + s.email = ["d.vorotilin@gmail.com"] + s.homepage = "https://github.com/machinio/cuprite" + s.summary = "Headless Chrome driver for Capybara" + s.description = "Culprite is a driver for Capybara that allows you to" \ + "run your tests on a headless Chrome browser" + s.license = "MIT" + s.require_paths = ["lib/capybara"] + s.files = Dir["{lib}/**/*"] + + s.add_runtime_dependency "capybara", ">= 2.1", "< 4" + s.add_runtime_dependency "websocket-driver", "~> 0.7" + s.add_runtime_dependency "cliver", "~> 0.3" + + s.add_development_dependency "image_size", "~> 2.0" + s.add_development_dependency "pdf-reader", "~> 2.1" + s.add_development_dependency "rake", "~> 12.3" + s.add_development_dependency "rspec", "~> 3.7" + s.add_development_dependency "sinatra", "~> 2.0" + s.add_development_dependency "launchy", "~> 2.4" + s.add_development_dependency "byebug", "~> 10.0" +end diff --git a/lib/capybara/cuprite.rb b/lib/capybara/cuprite.rb new file mode 100644 index 0000000..0c3638a --- /dev/null +++ b/lib/capybara/cuprite.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "capybara" + +module Capybara::Cuprite + require "cuprite/driver" + require "cuprite/browser" + require "cuprite/node" + require "cuprite/errors" + require "cuprite/cookie" + + class << self + def windows? + RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/ + end + + def mri? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby" + end + end +end + +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new(app) +end diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb new file mode 100644 index 0000000..ae78f7c --- /dev/null +++ b/lib/capybara/cuprite/browser.rb @@ -0,0 +1,401 @@ +require "forwardable" +require "cuprite/browser/process" +require "cuprite/browser/client" + +module Capybara::Cuprite + class Browser + extend Forwardable + + def self.start(*args) + new(*args) + end + + delegate [:command, :wait] => :@client + + def initialize(options = nil) + options ||= {} + @process = Process.start(options.fetch(:browser, {})) + @client = Client.new(@process.host, @process.port) + end + + def visit(url) + command("Page.enable") + command("DOM.enable") + command("CSS.enable") + # command("Page.setLifecycleEventsEnabled", enabled: true) + # command("Runtime.enable") + + command("Page.navigate", url: url) do |response| + wait(event: "Page.frameStoppedLoading", params: { frameId: response["frameId"] }) + end + end + + def current_url + command "current_url" + end + + def frame_url + command "frame_url" + end + + def status_code + command "status_code" + end + + def body + response = command "DOM.getDocument", depth: 0 + response = command "DOM.getOuterHTML", nodeId: response["root"]["nodeId"] + response["outerHTML"] + end + + def source + command "source" + end + + def title + command "title" + end + + def frame_title + command "frame_title" + end + + def parents(page_id, id) + command "parents", page_id, id + end + + def find(_, selector) + response = command("DOM.getDocument", depth: 0) + response = command("DOM.querySelectorAll", nodeId: response["root"]["nodeId"], selector: selector) + result = response["nodeIds"].map do |id| + node = command("DOM.describeNode", nodeId: id)["node"] + node["nodeId"] = id + node["selector"] = selector + [nil, node] # FIXME: page_id + end + + Array(result) + end + + def find_within(page_id, id, method, selector) + command "find_within", page_id, id, method, selector + end + + def all_text(page_id, id) + command "all_text", page_id, id + end + + def visible_text(page_id, node) + command "Runtime.evaluate", expression: <<~JS + $("#{node["selector"]}") + JS + + # command "visible_text", page_id, id + end + + def delete_text(page_id, id) + command "delete_text", page_id, id + end + + def property(page_id, id, name) + command "property", page_id, id, name.to_s + end + + def attributes(page_id, id) + command "attributes", page_id, id + end + + def attribute(page_id, id, name) + command "attribute", page_id, id, name.to_s + end + + def value(page_id, id) + command "value", page_id, id + end + + def set(page_id, id, value) + command "set", page_id, id, value + end + + def select_file(page_id, id, value) + command "select_file", page_id, id, value + end + + def tag_name(page_id, id) + command("tag_name", page_id, id).downcase + end + + def visible?(page_id, node) + response = command "CSS.getComputedStyleForNode", nodeId: node["nodeId"] + style = response["computedStyle"] + display = style.find { |s| s["name"] == "display" }["value"] + visibility = style.find { |s| s["name"] == "visibility" }["value"] + opacity = style.find { |s| s["name"] == "opacity" }["value"] + display == "none" || visibility == "hidden" || opacity == 0 ? false : true + end + + def disabled?(page_id, id) + command "disabled", page_id, id + end + + def click_coordinates(x, y) + command "click_coordinates", x, y + end + + def evaluate(script, *args) + command "evaluate", script, *args + end + + def evaluate_async(script, wait_time, *args) + command "evaluate_async", script, wait_time, *args + end + + def execute(script, *args) + command "execute", script, *args + end + + def within_frame(handle) + if handle.is_a?(Capybara::Node::Base) + command "push_frame", [handle.native.page_id, handle.native.id] + else + command "push_frame", handle + end + + yield + ensure + command "pop_frame" + end + + def switch_to_frame(handle) + case handle + when Capybara::Node::Base + command "push_frame", [handle.native.page_id, handle.native.id] + when :parent + command "pop_frame" + when :top + command "pop_frame", true + end + end + + def window_handle + command "window_handle" + end + + def window_handles + command "window_handles" + end + + def switch_to_window(handle) + command "switch_to_window", handle + end + + def open_new_window + command "open_new_window" + end + + def close_window(handle) + command "close_window", handle + end + + def find_window_handle(locator) + return locator if window_handles.include? locator + + handle = command "window_handle", locator + raise NoSuchWindowError unless handle + handle + end + + def within_window(locator) + original = window_handle + handle = find_window_handle(locator) + switch_to_window(handle) + yield + ensure + switch_to_window(original) + end + + def click(page_id, id, keys = [], offset = {}) + command "click", page_id, id, keys, offset + end + + def right_click(page_id, id, keys = [], offset = {}) + command "right_click", page_id, id, keys, offset + end + + def double_click(page_id, id, keys = [], offset = {}) + command "double_click", page_id, id, keys, offset + end + + def hover(page_id, id) + command "hover", page_id, id + end + + def drag(page_id, id, other_id) + command "drag", page_id, id, other_id + end + + def drag_by(page_id, id, x, y) + command "drag_by", page_id, id, x, y + end + + def select(page_id, id, value) + command "select", page_id, id, value + end + + def trigger(page_id, id, event) + command "trigger", page_id, id, event.to_s + end + + def reset + # command "reset" + restart + end + + def scroll_to(left, top) + command "scroll_to", left, top + end + + def render(path, options = {}) + check_render_options!(options) + options[:full] = !!options[:full] + command "render", path.to_s, options + end + + def render_base64(format, options = {}) + check_render_options!(options) + options[:full] = !!options[:full] + command "render_base64", format.to_s, options + end + + def set_zoom_factor(zoom_factor) + command "set_zoom_factor", zoom_factor + end + + def set_paper_size(size) + command "set_paper_size", size + end + + def resize(width, height) + command "resize", width, height + end + + def send_keys(page_id, id, keys) + command "send_keys", page_id, id, normalize_keys(keys) + end + + def path(page_id, id) + command "path", page_id, id + end + + def network_traffic(type = nil) + end + + def clear_network_traffic + command("clear_network_traffic") + end + + def set_proxy(ip, port, type, user, password) + args = [ip, port, type] + args << user if user + args << password if password + command("set_proxy", *args) + end + + def equals(page_id, id, other_id) + command("equals", page_id, id, other_id) + end + + def get_headers + command "get_headers" + end + + def set_headers(headers) + command "set_headers", headers + end + + def add_headers(headers) + command "add_headers", headers + end + + def add_header(header, options = {}) + command "add_header", header, options + end + + def response_headers + command "response_headers" + end + + def cookies + Hash[command("cookies").map { |cookie| [cookie["name"], Cookie.new(cookie)] }] + end + + def set_cookie(cookie) + cookie[:expires] = cookie[:expires].to_i * 1000 if cookie[:expires] + command "set_cookie", cookie + end + + def remove_cookie(name) + command "remove_cookie", name + end + + def clear_cookies + command "clear_cookies" + end + + def cookies_enabled=(flag) + command "cookies_enabled", !!flag + end + + def set_http_auth(user, password) + command "set_http_auth", user, password + end + + def page_settings=(settings) + command "set_page_settings", settings + end + + def url_whitelist=(whitelist) + command "set_url_whitelist", *whitelist + end + + def url_blacklist=(blacklist) + command "set_url_blacklist", *blacklist + end + + def clear_memory_cache + command "clear_memory_cache" + end + + def go_back + command "go_back" + end + + def go_forward + command "go_forward" + end + + def refresh + command "refresh" + end + + def accept_confirm + command "set_confirm_process", true + end + + def dismiss_confirm + command "set_confirm_process", false + end + + def accept_prompt(response) + command "set_prompt_response", response || false + end + + def dismiss_prompt + command "set_prompt_response", nil + end + + def modal_message + command "modal_message" + end + end +end diff --git a/lib/capybara/cuprite/browser/client.rb b/lib/capybara/cuprite/browser/client.rb new file mode 100644 index 0000000..4b366f8 --- /dev/null +++ b/lib/capybara/cuprite/browser/client.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "json" +require "net/http" +require "time" + +require "cuprite/browser/web_socket" + +module Capybara::Cuprite + class Browser + class Client + def initialize(host, port) + @host, @port = host, port + @ws = WebSocket.new(ws_url) + end + + def command(method, params = {}) + command_id = @ws.send(method: method, params: params) + message = wait(command: command_id)["result"] + yield message if block_given? + message + rescue DeadClient + restart + raise + end + + def wait(sec = 0.1, command: nil, event: nil, params: {}) + loop do + args = command ? { id: command } : { method: event } + message = @ws.message_by(args) + return message if command && message + return message if event && message && params.all? { |k, v| message["params"][k.to_s] == v } + sleep(sec) + end + end + + private + + def ws_url(try = 0) + @ws_url ||= try(Errno::ECONNREFUSED) do + response = Net::HTTP.get(@host, "/json", @port) + JSON.parse(response)[0]["webSocketDebuggerUrl"] + end + end + + def try(error, attempt = 0, attempts = 5) + yield + rescue error + attempt += 1 + sec = 0.1 + attempt / 10.0 + sleep(sec) and retry if attempt < attempts + end + end + end +end diff --git a/lib/capybara/cuprite/browser/process.rb b/lib/capybara/cuprite/browser/process.rb new file mode 100644 index 0000000..7c074ab --- /dev/null +++ b/lib/capybara/cuprite/browser/process.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require "cliver" + +module Capybara::Cuprite + class Browser + class Process + KILL_TIMEOUT = 2 + + BROWSER_PATH = "chrome" + BROWSER_HOST = "127.0.0.1" + BROWSER_PORT = "9222" + + # Chromium command line options + # https://peter.sh/experiments/chromium-command-line-switches/ + DEFAULT_OPTIONS = { + "headless" => true, + "disable-gpu" => true, + "window-size" => "1024,768", + "remote-debugging-port" => BROWSER_PORT, + "remote-debugging-address" => BROWSER_HOST + }.freeze + + def self.start(*args) + new(*args).tap { |s| s.start } + end + + def self.process_killer(pid) + proc do + begin + if Capybara::Cuprite.windows? + ::Process.kill("KILL", pid) + else + ::Process.kill("TERM", pid) + start = Time.now + while ::Process.wait(pid, ::Process::WNOHANG).nil? + sleep 0.05 + next unless (Time.now - start) > KILL_TIMEOUT + ::Process.kill("KILL", pid) + ::Process.wait(pid) + break + end + end + rescue Errno::ESRCH, Errno::ECHILD + end + end + end + + attr_reader :host, :port + + def initialize(options) + @logger = options[:logger] + @path = Cliver.detect(options[:path] || BROWSER_PATH) + + options = options.reject { |k, _| %i(logger path).include?(k) } + @options = DEFAULT_OPTIONS.merge(options) + + @host = @options["remote-debugging-address"] + @port = @options["remote-debugging-port"] + end + + def start + @read_io, @write_io = IO.pipe + + if @logger + @out_thread = Thread.new do + while !@read_io.eof? && (data = @read_io.readpartial(1024)) + @logger.write(data) + end + end + end + + process_options = { in: File::NULL } + process_options[:pgroup] = true unless Capybara::Cuprite.windows? + if Capybara::Cuprite.mri? + process_options[:out] = process_options[:err] = @write_io + end + + redirect_stdout do + cmd = [@path] + @options.map { |k, v| v == true ? "--#{k}" : "--#{k}=#{v}" if v } + @pid = ::Process.spawn(*cmd, process_options) + ObjectSpace.define_finalizer(self, self.class.process_killer(@pid)) + end + end + + def stop + return unless @pid + kill + @out_thread.kill if @logger + close_io + ObjectSpace.undefine_finalizer(self) + end + + def restart + stop + start + end + + private + + def redirect_stdout + if Capybara::Cuprite.mri? + yield + else + begin + prev = STDOUT.dup + $stdout = @write_io + STDOUT.reopen(@write_io) + yield + ensure + STDOUT.reopen(prev) + $stdout = STDOUT + prev.close + end + end + end + + def kill + self.class.process_killer(@pid).call + @pid = nil + end + + def close_io + [@write_io, @read_io].each do |io| + begin + io.close unless io.closed? + rescue IOError + raise unless RUBY_ENGINE == 'jruby' + end + end + end + end + end +end diff --git a/lib/capybara/cuprite/browser/web_socket.rb b/lib/capybara/cuprite/browser/web_socket.rb new file mode 100644 index 0000000..0f7d117 --- /dev/null +++ b/lib/capybara/cuprite/browser/web_socket.rb @@ -0,0 +1,80 @@ +require "json" +require "socket" +require "websocket/driver" + +module Capybara::Cuprite + class Browser + class WebSocket + attr_reader :url, :messages + + def initialize(url) + @url = url + uri = URI.parse(@url) + @sock = TCPSocket.new(uri.host, uri.port) + @driver = ::WebSocket::Driver.client(self) + + @messages = [] + @dead = false + @command_id = 0 + + @driver.on(:message, &method(:on_message)) + @driver.on(:error, &method(:on_error)) + @driver.on(:close, &method(:on_close)) + + Thread.abort_on_exception = true + + @thread = Thread.new do + @driver.parse(@sock.readpartial(512)) until @dead + end + + @driver.start + end + + def send(data) + next_command_id.tap do |id| + data = data.merge(id: id) + json = data.to_json + log ">>> #{json}" + @driver.text(json) + end + end + + def on_message(event) + log "<<< #{event.data}\n\n" + data = JSON.parse(event.data) + raise data["error"]["message"] if data["error"] + @messages << data + end + + def on_error(event) + raise e.message + end + + def on_close(event) + log("<<< #{event.code}, #{event.reason}\n\n") + @dead = true + @thread.kill + end + + def message_by(id: nil, method: nil) + @messages.find do |message| + id ? message["id"] == id : message["method"] == method + end + end + + def write(data) + @sock.write(data) + end + + private + + def next_command_id + @command_id += 1 + end + + def log(message) + puts(message) + end + end + end +end diff --git a/lib/capybara/cuprite/cookie.rb b/lib/capybara/cuprite/cookie.rb new file mode 100644 index 0000000..bdd44c4 --- /dev/null +++ b/lib/capybara/cuprite/cookie.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Capybara::Cuprite + class Cookie + def initialize(attributes) + @attributes = attributes + end + + def name + @attributes["name"] + end + + def value + @attributes["value"] + end + + def domain + @attributes["domain"] + end + + def path + @attributes["path"] + end + + def secure? + @attributes["secure"] + end + + def httponly? + @attributes["httponly"] + end + + def samesite + @attributes["samesite"] + end + + def expires + Time.at @attributes["expiry"] if @attributes["expiry"] + end + end +end diff --git a/lib/capybara/cuprite/driver.rb b/lib/capybara/cuprite/driver.rb new file mode 100644 index 0000000..811218c --- /dev/null +++ b/lib/capybara/cuprite/driver.rb @@ -0,0 +1,397 @@ +# frozen_string_literal: true + +require "uri" + +module Capybara::Cuprite + class Driver < Capybara::Driver::Base + DEFAULT_TIMEOUT = 30 + + attr_reader :app + + def initialize(app, options = {}) + @app = app + @options = options.freeze + @started = false + end + + def needs_server? + true + end + + def browser + @browser ||= Browser.start(@options) + end + + def restart + browser.restart + end + + def quit + browser.stop + end + + def visit(url) + @started = true + browser.visit(url) + end + + def current_url + if Capybara::VERSION.to_f < 3.0 + frame_url + else + browser.current_url + end + end + + def frame_url + browser.frame_url + end + + def status_code + browser.status_code + end + + def html + browser.body + end + alias_method :body, :html + + def source + browser.source.to_s + end + + def title + if Capybara::VERSION.to_f < 3.0 + frame_title + else + browser.title + end + end + + def frame_title + browser.frame_title + end + + def find(method, selector) + browser.find(method, selector).map { |page_id, id| Node.new(self, page_id, id) } + end + + def find_xpath(selector) + find :xpath, selector + end + + def find_css(selector) + find :css, selector + end + + def click(x, y) + browser.click_coordinates(x, y) + end + + def evaluate_script(script, *args) + result = browser.evaluate(script, *native_args(args)) + unwrap_script_result(result) + end + + def evaluate_async_script(script, *args) + result = browser.evaluate_async(script, session_wait_time, *native_args(args)) + unwrap_script_result(result) + end + + def execute_script(script, *args) + browser.execute(script, *native_args(args)) + nil + end + + def within_frame(name, &block) + browser.within_frame(name, &block) + end + + def switch_to_frame(locator) + browser.switch_to_frame(locator) + end + + def current_window_handle + browser.window_handle + end + + def window_handles + browser.window_handles + end + + def close_window(handle) + browser.close_window(handle) + end + + def open_new_window + browser.open_new_window + end + + def switch_to_window(handle) + browser.switch_to_window(handle) + end + + def within_window(name, &block) + browser.within_window(name, &block) + end + + def no_such_window_error + NoSuchWindowError + end + + def reset! + browser.reset + browser.url_blacklist = @options[:url_blacklist] if @options.key?(:url_blacklist) + browser.url_whitelist = @options[:url_whitelist] if @options.key?(:url_whitelist) + @started = false + end + + def save_screenshot(path, options = {}) + browser.render(path, options) + end + alias_method :render, :save_screenshot + + def render_base64(format = :png, options = {}) + browser.render_base64(format, options) + end + + def paper_size=(size = {}) + browser.set_paper_size(size) + end + + def zoom_factor=(zoom_factor) + browser.set_zoom_factor(zoom_factor) + end + + def resize(width, height) + browser.resize(width, height) + end + alias_method :resize_window, :resize + + def resize_window_to(handle, width, height) + within_window(handle) do + resize(width, height) + end + end + + def maximize_window(handle) + resize_window_to(handle, *screen_size) + end + + def window_size(handle) + within_window(handle) do + evaluate_script("[window.innerWidth, window.innerHeight]") + end + end + + def scroll_to(left, top) + browser.scroll_to(left, top) + end + + def network_traffic(type = nil) + browser.network_traffic(type) + end + + def clear_network_traffic + browser.clear_network_traffic + end + + def set_proxy(ip, port, type = "http", user = nil, password = nil) + browser.set_proxy(ip, port, type, user, password) + end + + def headers + browser.get_headers + end + + def headers=(headers) + browser.set_headers(headers) + end + + def add_headers(headers) + browser.add_headers(headers) + end + + def add_header(name, value, options = {}) + browser.add_header({ name => value }, { permanent: true }.merge(options)) + end + + def response_headers + browser.response_headers + end + + def cookies + browser.cookies + end + + def set_cookie(name, value, options = {}) + options[:name] ||= name + options[:value] ||= value + options[:domain] ||= begin + if @started + URI.parse(browser.current_url).host + else + URI.parse(default_cookie_host).host || "127.0.0.1" + end + end + + browser.set_cookie(options) + end + + def remove_cookie(name) + browser.remove_cookie(name) + end + + def clear_cookies + browser.clear_cookies + end + + def cookies_enabled=(flag) + browser.cookies_enabled = flag + end + + def clear_memory_cache + browser.clear_memory_cache + end + + # * Browser with set settings does not send `Authorize` on POST request + # * With manually set header browser makes next request with + # `Authorization: Basic Og==` header when settings are empty and the + # response was `401 Unauthorized` (which means Base64.encode64(":")). + # Combining both methods to reach proper behavior. + def basic_authorize(user, password) + browser.set_http_auth(user, password) + credentials = ["#{user}:#{password}"].pack("m*").strip + add_header("Authorization", "Basic #{credentials}") + end + + def pause + # STDIN is not necessarily connected to a keyboard. It might even be closed. + # So we need a method other than keypress to continue. + + # In jRuby - STDIN returns immediately from select + # see https://github.com/jruby/jruby/issues/1783 + read, write = IO.pipe + Thread.new { IO.copy_stream(STDIN, write); write.close } + + STDERR.puts "Cuprite execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue." + + signal = false + old_trap = trap("SIGCONT") { signal = true; STDERR.puts "\nSignal SIGCONT received" } + keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received + + unless signal + begin + input = read.read_nonblock(80) # clear out the read buffer + puts unless input&.end_with?("\n") + rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN. + end + end + ensure + trap("SIGCONT", old_trap) # Restore the previous signal handler, if there was one. + STDERR.puts "Continuing" + end + + def wait? + true + end + + def invalid_element_errors + [Capybara::Cuprite::ObsoleteNode, Capybara::Cuprite::MouseEventFailed] + end + + def go_back + browser.go_back + end + + def go_forward + browser.go_forward + end + + def refresh + browser.refresh + end + + def accept_modal(type, options = {}) + case type + when :confirm + browser.accept_confirm + when :prompt + browser.accept_prompt options[:with] + end + + yield if block_given? + + find_modal(options) + end + + def dismiss_modal(type, options = {}) + case type + when :confirm + browser.dismiss_confirm + when :prompt + browser.dismiss_prompt + end + + yield if block_given? + find_modal(options) + end + + private + + def native_args(args) + args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.native : arg } + end + + def screen_size + @options[:screen_size] || [1366, 768] + end + + def find_modal(options) + start_time = Time.now + timeout_sec = options.fetch(:wait) { session_wait_time } + expect_text = options[:text] + expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s) + not_found_msg = "Unable to find modal dialog" + not_found_msg += " with #{expect_text}" if expect_text + + begin + modal_text = browser.modal_message + raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp)) + rescue Capybara::ModalNotFound => e + raise e, not_found_msg if (Time.now - start_time) >= timeout_sec + sleep(0.05) + retry + end + modal_text + end + + def session_wait_time + if respond_to?(:session_options) + session_options.default_max_wait_time + else + begin Capybara.default_max_wait_time rescue Capybara.default_wait_time end + end + end + + def default_cookie_host + if respond_to?(:session_options) + session_options.app_host + else + Capybara.app_host + end || "" + end + + def unwrap_script_result(arg) + case arg + when Array + arg.map { |e| unwrap_script_result(e) } + when Hash + return Capybara::Cuprite::Node.new(self, arg["ELEMENT"]["page_id"], arg["ELEMENT"]["id"]) if arg["ELEMENT"] + arg.each { |k, v| arg[k] = unwrap_script_result(v) } + else + arg + end + end + end +end diff --git a/lib/capybara/cuprite/errors.rb b/lib/capybara/cuprite/errors.rb new file mode 100644 index 0000000..fa41b2a --- /dev/null +++ b/lib/capybara/cuprite/errors.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +module Capybara + module Cuprite + class Error < StandardError; end + class NoSuchWindowError < Error; end + + class ClientError < Error + attr_reader :response + + def initialize(response) + @response = response + end + end + + class JSErrorItem + attr_reader :message, :stack + + def initialize(message, stack) + @message = message + @stack = stack + end + + def to_s + [message, stack].join("\n") + end + end + + class BrowserError < ClientError + def name + response["name"] + end + + def error_parameters + response["args"].join("\n") + end + + def message + "There was an error inside the browser of Cuprite. " \ + "If this is the error returned, and not the cause of a more detailed error response, " \ + "this is probably a bug, so please report it. " \ + "\n\n#{name}: #{error_parameters}" + end + end + + class JavascriptError < ClientError + def javascript_errors + response["args"].first.map { |data| JSErrorItem.new(data["message"], data["stack"]) } + end + + def message + "One or more errors were raised in the Javascript code on the page. " \ + "If you do not care about these errors, you can ignore them by " \ + "setting js_errors: false in your Cuprite configuration (see " \ + "documentation for details)." \ + "\n\n#{javascript_errors.map(&:to_s).join("\n")}" + end + end + + class StatusFailError < ClientError + def url + response["args"].first + end + + def details + response["args"][1] + end + + def message + msg = "Request to "#{url}" failed to reach server, check DNS and/or server status" + msg += " - #{details}" if details + msg + end + end + + class FrameNotFound < ClientError + def name + response["args"].first + end + + def message + "The frame "#{name}" was not found." + end + end + + class InvalidSelector < ClientError + def method + response["args"][0] + end + + def selector + response["args"][1] + end + + def message + "The browser raised a syntax error while trying to evaluate " \ + "#{method} selector #{selector.inspect}" + end + end + + class NodeError < ClientError + attr_reader :node + + def initialize(node, response) + @node = node + super(response) + end + end + + class ObsoleteNode < NodeError + def message + "The element you are trying to interact with is either not part of the DOM, or is " \ + "not currently visible on the page (perhaps display: none is set). " \ + "It is possible the element has been replaced by another element and you meant to interact with " \ + "the new element. If so you need to do a new find in order to get a reference to the " \ + "new element." + end + end + + class MouseEventFailed < NodeError + def name + response["args"][0] + end + + def selector + response["args"][1] + end + + def position + [response["args"][2]["x"], response["args"][2]["y"]] + end + + def message + "Firing a #{name} at co-ordinates [#{position.join(", ")}] failed. Cuprite detected " \ + "another element with CSS selector \"#{selector}\" at this position. " \ + "It may be overlapping the element you are trying to interact with. " \ + "If you don't care about overlapping elements, try using node.trigger(\"#{name}\")." + end + end + + class KeyError < ::ArgumentError + def initialize(response) + super(response["args"].first) + end + end + + class TimeoutError < Error + def initialize(message) + @message = message + end + + def message + "Timed out waiting for response to #{@message}. It's possible that this happened " \ + "because something took a very long time (for example a page load was slow). " \ + "If so, setting the Cuprite :timeout option to a higher value will help " \ + "(see the docs for details). If increasing the timeout does not help, this is " \ + "probably a bug in Cuprite - please report it to the issue tracker." + end + end + + class ScriptTimeoutError < Error + def message + "Timed out waiting for evaluated script to resturn a value" + end + end + + class DeadClient < Error + def initialize(message) + @message = message + end + + def message + "Browser client died while processing #{@message}" + end + end + end +end diff --git a/lib/capybara/cuprite/node.rb b/lib/capybara/cuprite/node.rb new file mode 100644 index 0000000..cc69b09 --- /dev/null +++ b/lib/capybara/cuprite/node.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +module Capybara::Cuprite + class Node < Capybara::Driver::Node + attr_reader :page_id, :id + + def initialize(driver, page_id, id) + super(driver, self) + + @page_id = page_id + @id = id + end + + def browser + driver.browser + end + + def command(name, *args) + browser.send(name, page_id, id, *args) + rescue BrowserError => error + case error.name + when "Cuprite.ObsoleteNode" + raise ObsoleteNode.new(self, error.response) + when "Cuprite.MouseEventFailed" + raise MouseEventFailed.new(self, error.response) + else + raise + end + end + + def parents + command(:parents).map { |parent_id| self.class.new(driver, page_id, parent_id) } + end + + def find(method, selector) + command(:find_within, method, selector).map { |id| self.class.new(driver, page_id, id) } + end + + def find_xpath(selector) + find :xpath, selector + end + + def find_css(selector) + find :css, selector + end + + def all_text + filter_text command(:all_text) + end + + def visible_text + if Capybara::VERSION.to_f < 3.0 + filter_text command(:visible_text) + else + command(:visible_text).to_s + .gsub(/\A[[:space:]&&[^\u00a0]]+/, "") + .gsub(/[[:space:]&&[^\u00a0]]+\z/, "") + .gsub(/\n+/, "\n") + .tr("\u00a0", " ") + end + end + + def property(name) + command :property, name + end + + def [](name) + # Although the attribute matters, the property is consistent. Return that in + # preference to the attribute for links and images. + if ((tag_name == "img") && (name == "src")) || ((tag_name == "a") && (name == "href")) + # if attribute exists get the property + return command(:attribute, name) && command(:property, name) + end + + value = property(name) + value = command(:attribute, name) if value.nil? || value.is_a?(Hash) + + value + end + + def attributes + command :attributes + end + + def value + command :value + end + + def set(value, options = {}) + warn "Options passed to Node#set but Cuprite doesn't currently support any - ignoring" unless options.empty? + + if tag_name == "input" + case self[:type] + when "radio" + click + when "checkbox" + click if value != checked? + when "file" + files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s + command :select_file, files + else + command :set, value.to_s + end + elsif tag_name == "textarea" + command :set, value.to_s + elsif self[:isContentEditable] + command :delete_text + send_keys(value.to_s) + end + end + + def select_option + command :select, true + end + + def unselect_option + command(:select, false) || + raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.") + end + + def tag_name + @tag_name ||= command(:tag_name) + end + + def visible? + command :visible? + end + + def checked? + self[:checked] + end + + def selected? + !!self[:selected] + end + + def disabled? + command :disabled? + end + + def click(keys = [], offset = {}) + command :click, keys, offset + end + + def right_click(keys = [], offset = {}) + command :right_click, keys, offset + end + + def double_click(keys = [], offset = {}) + command :double_click, keys, offset + end + + def hover + command :hover + end + + def drag_to(other) + command :drag, other.id + end + + def drag_by(x, y) + command :drag_by, x, y + end + + def trigger(event) + command :trigger, event + end + + def ==(other) + (page_id == other.page_id) && command(:equals, other.id) + end + + def send_keys(*keys) + command :send_keys, keys + end + alias_method :send_key, :send_keys + + def path + command :path + end + + # @api private + def to_json(*) + JSON.generate as_json + end + + # @api private + def as_json(*) + { ELEMENT: { page_id: @page_id, id: @id } } + end + + private + + def filter_text(text) + if Capybara::VERSION.to_f < 3 + Capybara::Helpers.normalize_whitespace(text.to_s) + else + text.gsub(/[\u200b\u200e\u200f]/, "") + .gsub(/[\ \n\f\t\v\u2028\u2029]+/, " ") + .gsub(/\A[[:space:]&&[^\u00a0]]+/, "") + .gsub(/[[:space:]&&[^\u00a0]]+\z/, "") + .tr("\u00a0", " ") + end + end + end +end diff --git a/lib/capybara/cuprite/version.rb b/lib/capybara/cuprite/version.rb new file mode 100644 index 0000000..133101d --- /dev/null +++ b/lib/capybara/cuprite/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Capybara + module Cuprite + VERSION = "0.1.0" + end +end diff --git a/spec/integration/driver_spec.rb b/spec/integration/driver_spec.rb new file mode 100644 index 0000000..94c2a61 --- /dev/null +++ b/spec/integration/driver_spec.rb @@ -0,0 +1,1547 @@ +# frozen_string_literal: true + +require "spec_helper" +require "image_size" +require "pdf/reader" + +module Capybara::Cuprite + describe Driver do + before do + @session = TestSessions::Cuprite + @driver = @session.driver + end + + after { @driver.reset! } + + def session_url(path) + server = @session.server + "http://#{server.host}:#{server.port}#{path}" + end + + it "supports a custom path" do + begin + file = PUMPKINHEAD_ROOT + "/spec/support/custom_chrome_called" + path = PUMPKINHEAD_ROOT + "/spec/support/custom_chrome" + + driver = Driver.new(nil, path: path) + driver.browser + + # If the correct custom path is called, it will touch the file. + # We allow at least 10 secs for this to happen before failing. + + tries = 0 + until File.exist?(file) || tries == 100 + sleep 0.1 + tries += 1 + end + + expect(File.exist?(file)).to be true + ensure + FileUtils.rm_f(file) + driver&.quit + end + end + + context "output redirection" do + let(:logger) { StringIO.new } + let(:session) { Capybara::Session.new(:cuprite_with_logger, TestApp) } + + before do + Capybara.register_driver :cuprite_with_logger do |app| + Capybara::Cuprite::Driver.new(app) + end + end + + after { session.driver.quit } + + it "supports capturing console.log" do + session.visit("/cuprite/console_log") + expect(logger.string).to include("Hello world") + end + + it "is threadsafe in how it captures console.log" do + pending("JRuby and Rubinius do not support the :out parameter to Process.spawn, so there is no threadsafe way to redirect output") unless Capybara::Cuprite.mri? + + # Write something to STDOUT right before Process.spawn is called + allow(Process).to receive(:spawn).and_wrap_original do |m, *args| + STDOUT.puts "1" + $stdout.puts "2" + m.call(*args) + end + + expect do + session.visit("/cuprite/console_log") + end.to output("1\n2\n").to_stdout_from_any_process + + expect(logger.string).not_to match(/\d/) + end + end + + it "raises an error and restarts the client if the client dies while executing a command" do + expect { @driver.browser.command("exit") }.to raise_error(DeadClient) + @session.visit("/") + expect(@driver.html).to include("Hello world") + end + + it "quits silently before visit call" do + driver = Capybara::Cuprite::Driver.new(nil) + expect { driver.quit }.not_to raise_error + end + + it "has a viewport size of 1024x768 by default" do + @session.visit("/") + expect( + @driver.evaluate_script("[window.innerWidth, window.innerHeight]") + ).to eq([1024, 768]) + end + + it "allows the viewport to be resized" do + @session.visit("/") + @driver.resize(200, 400) + expect( + @driver.evaluate_script("[window.innerWidth, window.innerHeight]") + ).to eq([200, 400]) + end + + it "defaults viewport maximization to 1366x768" do + @session.visit("/") + @session.current_window.maximize + expect(@session.current_window.size).to eq([1366, 768]) + end + + it "allows custom maximization size" do + begin + @driver.options[:screen_size] = [1600, 1200] + @session.visit("/") + @session.current_window.maximize + expect(@session.current_window.size).to eq([1600, 1200]) + ensure + @driver.options.delete(:screen_size) + end + end + + it "allows the page to be scrolled" do + @session.visit("/cuprite/long_page") + @driver.resize(10, 10) + @driver.scroll_to(200, 100) + expect( + @driver.evaluate_script("[window.scrollX, window.scrollY]") + ).to eq([200, 100]) + end + + it "supports specifying viewport size with an option" do + begin + Capybara.register_driver :cuprite_with_custom_window_size do |app| + Capybara::Cuprite::Driver.new( + app, + logger: TestSessions.logger, + window_size: [800, 600] + ) + end + driver = Capybara::Session.new(:cuprite_with_custom_window_size, TestApp).driver + driver.visit(session_url("/")) + expect( + driver.evaluate_script("[window.innerWidth, window.innerHeight]") + ).to eq([800, 600]) + ensure + driver&.quit + end + end + + shared_examples "render screen" do + it "supports rendering the whole of a page that goes outside the viewport" do + @session.visit("/cuprite/long_page") + + create_screenshot file + File.open(file, "rb") do |f| + expect(ImageSize.new(f.read).size).to eq( + @driver.evaluate_script("[window.innerWidth, window.innerHeight]") + ) + end + + create_screenshot file, full: true + File.open(file, "rb") do |f| + expect(ImageSize.new(f.read).size).to eq( + @driver.evaluate_script("[document.documentElement.clientWidth, document.documentElement.clientHeight]") + ) + end + end + + it "supports rendering the entire window when documentElement has no height" do + @session.visit("/cuprite/fixed_positioning") + + create_screenshot file, full: true + File.open(file, "rb") do |f| + expect(ImageSize.new(f.read).size).to eq( + @driver.evaluate_script("[window.innerWidth, window.innerHeight]") + ) + end + end + + it "supports rendering just the selected element" do + @session.visit("/cuprite/long_page") + + create_screenshot file, selector: "#penultimate" + + File.open(file, "rb") do |f| + size = @driver.evaluate_script <<-JS + function() { + var ele = document.getElementById("penultimate"); + var rect = ele.getBoundingClientRect(); + return [rect.width, rect.height]; + }(); + JS + expect(ImageSize.new(f.read).size).to eq(size) + end + end + + it "ignores :selector in #save_screenshot if full: true" do + @session.visit("/cuprite/long_page") + expect(@driver.browser).to receive(:warn).with(/Ignoring :selector/) + + create_screenshot file, full: true, selector: "#penultimate" + + File.open(file, "rb") do |f| + expect(ImageSize.new(f.read).size).to eq( + @driver.evaluate_script("[document.documentElement.clientWidth, document.documentElement.clientHeight]") + ) + end + end + + it "resets element positions after" do + @session.visit("cuprite/long_page") + el = @session.find(:css, "#middleish") + # make the page scroll an element into view + el.click + position_script = "document.querySelector("#middleish").getBoundingClientRect()" + offset = @session.evaluate_script(position_script) + create_screenshot file + expect(@session.evaluate_script(position_script)).to eq offset + end + end + + describe "#save_screenshot" do + let(:format) { :png } + let(:file) { PUMPKINHEAD_ROOT + "/spec/tmp/screenshot.#{format}" } + + before(:each) { FileUtils.rm_f file } + + def create_screenshot(file, *args) + @driver.save_screenshot(file, *args) + end + + it "supports rendering the page" do + @session.visit("/") + + @driver.save_screenshot(file) + + expect(File.exist?(file)).to be true + end + + it "supports rendering the page with a nonstring path" do + @session.visit("/") + + @driver.save_screenshot(Pathname(file)) + + expect(File.exist?(file)).to be true + end + + it "supports rendering the page to file without extension when format is specified" do + begin + file = PUMPKINHEAD_ROOT + "/spec/tmp/screenshot" + FileUtils.rm_f file + @session.visit("/") + + @driver.save_screenshot(file, format: "jpg") + + expect(File.exist?(file)).to be true + ensure + FileUtils.rm_f file + end + end + + it "supports rendering the page with different quality settings" do + file2 = PUMPKINHEAD_ROOT + "/spec/tmp/screenshot2.#{format}" + file3 = PUMPKINHEAD_ROOT + "/spec/tmp/screenshot3.#{format}" + FileUtils.rm_f [file2, file3] + + begin + @session.visit("/") + @driver.save_screenshot(file, quality: 0) + @driver.save_screenshot(file2) # defaults to a quality of 75 + @driver.save_screenshot(file3, quality: 100) + expect(File.size(file)).to be < File.size(file2) + expect(File.size(file2)).to be < File.size(file3) + ensure + FileUtils.rm_f [file2, file3] + end + end + + shared_examples "when #zoom_factor= is set" do + let(:format) { :xbm } + + it "changes image dimensions" do + @session.visit("/cuprite/zoom_test") + + black_pixels_count = lambda { |file| + File.read(file).to_s[/{.*}/m][1...-1].split(/\W/).map { |n| n.hex.to_s(2).count("1") }.reduce(:+) + } + @driver.save_screenshot(file) + before = black_pixels_count[file] + + @driver.zoom_factor = zoom_factor + @driver.save_screenshot(file) + after = black_pixels_count[file] + + expect(after.to_f / before.to_f).to eq(zoom_factor**2) + end + end + + context "zoom in" do + let(:zoom_factor) { 2 } + include_examples "when #zoom_factor= is set" + end + + context "zoom out" do + let(:zoom_factor) { 0.5 } + include_examples "when #zoom_factor= is set" + end + + context "when #paper_size= is set" do + let(:format) { :pdf } + + it "changes pdf size" do + @session.visit("/cuprite/long_page") + @driver.paper_size = { width: "1in", height: "1in" } + + @driver.save_screenshot(file) + + reader = PDF::Reader.new(file) + reader.pages.each do |page| + bbox = page.attributes[:MediaBox] + width = (bbox[2] - bbox[0]) / 72 + expect(width).to eq(1) + end + end + end + + include_examples "render screen" + end + + describe "#render_base64" do + let(:file) { PUMPKINHEAD_ROOT + "/spec/tmp/screenshot.#{format}" } + + def create_screenshot(file, *args) + image = @driver.render_base64(format, *args) + File.open(file, "wb") { |f| f.write Base64.decode64(image) } + end + + it "supports rendering the page in base64" do + @session.visit("/") + + screenshot = @driver.render_base64 + + expect(screenshot.length).to be > 100 + end + + context "png" do + let(:format) { :png } + include_examples "render screen" + end + + context "jpeg" do + let(:format) { :jpeg } + include_examples "render screen" + end + end + + context "setting headers" do + it "allows headers to be set" do + @driver.headers = { + "Cookie" => "foo=bar", + "Host" => "foo.com" + } + @session.visit("/cuprite/headers") + expect(@driver.body).to include("COOKIE: foo=bar") + expect(@driver.body).to include("HOST: foo.com") + end + + it "allows headers to be read" do + expect(@driver.headers).to eq({}) + @driver.headers = { "User-Agent" => "Browser", "Host" => "foo.com" } + expect(@driver.headers).to eq("User-Agent" => "Browser", "Host" => "foo.com") + end + + it "supports User-Agent" do + @driver.headers = { "User-Agent" => "foo" } + @session.visit "/" + expect(@driver.evaluate_script("window.navigator.userAgent")).to eq("foo") + end + + it "sets headers for all HTTP requests" do + @driver.headers = { "X-Omg" => "wat" } + @session.visit "/" + @driver.execute_script <<-JS + var request = new XMLHttpRequest(); + request.open("GET", "/cuprite/headers", false); + request.send(); + + if (request.status === 200) { + document.body.innerHTML = request.responseText; + } + JS + expect(@driver.body).to include("X_OMG: wat") + end + + it "adds new headers" do + @driver.headers = { "User-Agent" => "Browser", "Host" => "foo.com" } + @driver.add_headers("User-Agent" => "Cuprite", "Appended" => "true") + @session.visit("/cuprite/headers") + expect(@driver.body).to include("USER_AGENT: Cuprite") + expect(@driver.body).to include("HOST: foo.com") + expect(@driver.body).to include("APPENDED: true") + end + + it "sets headers on the initial request" do + @driver.headers = { "PermanentA" => "a" } + @driver.add_headers("PermanentB" => "b") + @driver.add_header("Referer", "http://google.com", permanent: false) + @driver.add_header("TempA", "a", permanent: false) + + @session.visit("/cuprite/headers_with_ajax") + initial_request = @session.find(:css, "#initial_request").text + ajax_request = @session.find(:css, "#ajax_request").text + + expect(initial_request).to include("PERMANENTA: a") + expect(initial_request).to include("PERMANENTB: b") + expect(initial_request).to include("REFERER: http://google.com") + expect(initial_request).to include("TEMPA: a") + + expect(ajax_request).to include("PERMANENTA: a") + expect(ajax_request).to include("PERMANENTB: b") + expect(ajax_request).to_not include("REFERER: http://google.com") + expect(ajax_request).to_not include("TEMPA: a") + end + + it "keeps added headers on redirects by default" do + @driver.add_header("X-Custom-Header", "1", permanent: false) + @session.visit("/cuprite/redirect_to_headers") + expect(@driver.body).to include("X_CUSTOM_HEADER: 1") + end + + it "does not keep added headers on redirect when " \ + "permanent is no_redirect" do + @driver.add_header("X-Custom-Header", "1", permanent: :no_redirect) + + @session.visit("/cuprite/redirect_to_headers") + expect(@driver.body).not_to include("X_CUSTOM_HEADER: 1") + end + + context "multiple windows" do + it "persists headers across popup windows" do + @driver.headers = { + "Cookie" => "foo=bar", + "Host" => "foo.com", + "User-Agent" => "foo" + } + @session.visit("/cuprite/popup_headers") + @session.click_link "pop up" + @session.switch_to_window @session.windows.last + expect(@driver.body).to include("USER_AGENT: foo") + expect(@driver.body).to include("COOKIE: foo=bar") + expect(@driver.body).to include("HOST: foo.com") + end + + it "sets headers in existing windows" do + @session.open_new_window + @driver.headers = { + "Cookie" => "foo=bar", + "Host" => "foo.com", + "User-Agent" => "foo" + } + @session.visit("/cuprite/headers") + expect(@driver.body).to include("USER_AGENT: foo") + expect(@driver.body).to include("COOKIE: foo=bar") + expect(@driver.body).to include("HOST: foo.com") + + @session.switch_to_window @session.windows.last + @session.visit("/cuprite/headers") + expect(@driver.body).to include("USER_AGENT: foo") + expect(@driver.body).to include("COOKIE: foo=bar") + expect(@driver.body).to include("HOST: foo.com") + end + + it "keeps temporary headers local to the current window" do + @session.open_new_window + @driver.add_header("X-Custom-Header", "1", permanent: false) + + @session.switch_to_window @session.windows.last + @session.visit("/cuprite/headers") + expect(@driver.body).not_to include("X_CUSTOM_HEADER: 1") + + @session.switch_to_window @session.windows.first + @session.visit("/cuprite/headers") + expect(@driver.body).to include("X_CUSTOM_HEADER: 1") + end + + it "does not mix temporary headers with permanent ones when propagating to other windows" do + @session.open_new_window + @driver.add_header("X-Custom-Header", "1", permanent: false) + @driver.add_header("Host", "foo.com") + + @session.switch_to_window @session.windows.last + @session.visit("/cuprite/headers") + expect(@driver.body).to include("HOST: foo.com") + expect(@driver.body).not_to include("X_CUSTOM_HEADER: 1") + + @session.switch_to_window @session.windows.first + @session.visit("/cuprite/headers") + expect(@driver.body).to include("HOST: foo.com") + expect(@driver.body).to include("X_CUSTOM_HEADER: 1") + end + + it "does not propagate temporary headers to new windows" do + @session.visit "/" + @driver.add_header("X-Custom-Header", "1", permanent: false) + @session.open_new_window + + @session.switch_to_window @session.windows.last + @session.visit("/cuprite/headers") + expect(@driver.body).not_to include("X_CUSTOM_HEADER: 1") + + @session.switch_to_window @session.windows.first + @session.visit("/cuprite/headers") + expect(@driver.body).to include("X_CUSTOM_HEADER: 1") + end + end + end + + it "supports clicking precise coordinates" do + @session.visit("/cuprite/click_coordinates") + @driver.click(100, 150) + expect(@driver.body).to include("x: 100, y: 150") + end + + it "supports executing multiple lines of javascript" do + @driver.execute_script <<-JS + var a = 1 + var b = 2 + window.result = a + b + JS + expect(@driver.evaluate_script("window.result")).to eq(3) + end + + it "operates a timeout when communicating with browser" do + begin + prev_timeout = @driver.timeout + @driver.timeout = 0.001 + expect { @driver.browser.command "noop" }.to raise_error(TimeoutError) + ensure + @driver.timeout = prev_timeout + end + end + + unless Capybara::Cuprite.windows? + # Not sure how to do this on Windows, so skipping + it "supports quitting the session" do + driver = Capybara::Cuprite::Driver.new(nil) + pid = driver.server_pid + + expect(Process.kill(0, pid)).to eq(1) + driver.quit + + expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH) + end + end + + context "extending browser javascript" do + before do + @extended_driver = Capybara::Cuprite::Driver.new( + @session.app, + logger: TestSessions.logger, + extensions: %W[#{File.expand_path "../support/geolocation.js", __dir__}] + ) + end + + after do + @extended_driver.quit + end + + it "supports extending the browser's world" do + @extended_driver.visit session_url("/cuprite/requiring_custom_extension") + expect( + @extended_driver.body + ).to include(%(Location: 1,-1)) + expect( + @extended_driver.evaluate_script(%(document.getElementById("location").innerHTML)) + ).to eq("1,-1") + expect( + @extended_driver.evaluate_script("navigator.geolocation") + ).to_not eq(nil) + end + + it "errors when extension is unavailable" do + begin + @failing_driver = Capybara::Cuprite::Driver.new( + @session.app, + logger: TestSessions.logger, + extensions: %W[#{File.expand_path "../support/non_existent.js", __dir__}] + ) + expect { @failing_driver.visit "/" }.to raise_error(Capybara::Cuprite::BrowserError, /Unable to load extension: .*non_existent\.js/) + ensure + @failing_driver.quit + end + end + end + + context "javascript errors" do + it "propagates a Javascript error inside Cuprite to a ruby exception" do + expect do + @driver.browser.command "browser_error" + end.to raise_error(BrowserError) { |e| + expect(e.message).to include("Error: zomg") + # 2.1 refers to files as being in code subdirectory + expect(e.message).to include("compiled/browser.js").or include("code/browser.js") + } + end + + it "propagates an asynchronous Javascript error on the page to a ruby exception" do + expect do + @driver.execute_script "setTimeout(function() { omg }, 0)" + sleep 0.01 + @driver.execute_script "" + end.to raise_error(JavascriptError, /ReferenceError.*omg/) + end + + it "propagates a synchronous Javascript error on the page to a ruby exception" do + expect do + @driver.execute_script "omg" + end.to raise_error(JavascriptError, /ReferenceError.*omg/) + end + + it "does not re-raise a Javascript error if it is rescued" do + expect do + @driver.execute_script "setTimeout(function() { omg }, 0)" + sleep 0.01 + @driver.execute_script "" + end.to raise_error(JavascriptError) + + # should not raise again + expect(@driver.evaluate_script("1+1")).to eq(2) + end + + it "propagates a Javascript error during page load to a ruby exception" do + expect { @session.visit "/cuprite/js_error" }.to raise_error(JavascriptError) + end + + it "does not propagate a Javascript error to ruby if error raising disabled" do + begin + driver = Capybara::Cuprite::Driver.new(@session.app, js_errors: false, logger: TestSessions.logger) + driver.visit session_url("/cuprite/js_error") + driver.execute_script "setTimeout(function() { omg }, 0)" + sleep 0.1 + expect(driver.body).to include("hello") + ensure + driver&.quit + end + end + + it "does not propagate a Javascript error to ruby if error raising disabled and client restarted" do + begin + driver = Capybara::Cuprite::Driver.new(@session.app, js_errors: false, logger: TestSessions.logger) + driver.restart + driver.visit session_url("/cuprite/js_error") + driver.execute_script "setTimeout(function() { omg }, 0)" + sleep 0.1 + expect(driver.body).to include("hello") + ensure + driver&.quit + end + end + + context "Browser page settings" do + it "can override defaults" do + begin + driver = Capybara::Cuprite::Driver.new(@session.app, page_settings: { userAgent: "PageSettingsOverride" }, logger: TestSessions.logger) + driver.visit session_url("/cuprite/headers") + expect(driver.body).to include("USER_AGENT: PageSettingsOverride") + ensure + driver&.quit + end + end + + it "can set resource timeout" do + begin + # If PJS resource timeout is less than drivers timeout it should ignore resources not loading in time + driver = Capybara::Cuprite::Driver.new(@session.app, page_settings: { resourceTimeout: 1000 }, logger: TestSessions.logger) + driver.timeout = 3 + expect do + driver.visit session_url("/cuprite/visit_timeout") + end.not_to raise_error + ensure + driver&.quit + end + end + end + end + + context "browser {'status': 'fail'} responses" do + before { @port = @session.server.port } + + it "do not occur when DNS correct" do + expect { @session.visit("http://localhost:#{@port}/") }.not_to raise_error + end + + it "handles when DNS incorrect" do + expect { @session.visit("http://nope:#{@port}/") }.to raise_error(StatusFailError) + end + + it "has a descriptive message when DNS incorrect" do + url = "http://nope:#{@port}/" + expect do + @session.visit(url) + end.to raise_error(StatusFailError, %(Request to "#{url}" failed to reach server, check DNS and/or server status)) + end + + it "reports open resource requests" do + old_timeout = @session.driver.timeout + begin + @session.driver.timeout = 2 + expect do + @session.visit("/cuprite/visit_timeout") + end.to raise_error(StatusFailError, %r{resources still waiting http://.*/cuprite/really_slow}) + ensure + @session.driver.timeout = old_timeout + end + end + + it "doesnt report open resources where there are none" do + old_timeout = @session.driver.timeout + begin + @session.driver.timeout = 2 + expect do + @session.visit("/cuprite/really_slow") + end.to raise_error(StatusFailError) { |error| + expect(error.message).not_to include("resources still waiting") + } + ensure + @session.driver.timeout = old_timeout + end + end + end + + context "network traffic" do + before do + @driver.restart + end + + it "keeps track of network traffic" do + @session.visit("/cuprite/with_js") + urls = @driver.network_traffic.map(&:url) + + expect(urls.grep(%r{/cuprite/jquery.min.js$}).size).to eq(1) + expect(urls.grep(%r{/cuprite/jquery-ui.min.js$}).size).to eq(1) + expect(urls.grep(%r{/cuprite/test.js$}).size).to eq(1) + end + + it "keeps track of blocked network traffic" do + @driver.browser.url_blacklist = ["unwanted"] + + @session.visit "/cuprite/url_blacklist" + + blocked_urls = @driver.network_traffic(:blocked).map(&:url) + + expect(blocked_urls).to include(/unwanted/) + end + + it "captures responses" do + @session.visit("/cuprite/with_js") + request = @driver.network_traffic.last + + expect(request.response_parts.last.status).to eq(200) + end + + it "captures errors" do + @session.visit("/cuprite/with_ajax_fail") + expect(@session).to have_css("h1", text: "Done") + error = @driver.network_traffic.last.error + + expect(error).to be + end + + it "keeps a running list between multiple web page views" do + @session.visit("/cuprite/with_js") + expect(@driver.network_traffic.length).to eq(4) + + @session.visit("/cuprite/with_js") + expect(@driver.network_traffic.length).to eq(8) + end + + it "gets cleared on restart" do + @session.visit("/cuprite/with_js") + expect(@driver.network_traffic.length).to eq(4) + + @driver.restart + + @session.visit("/cuprite/with_js") + expect(@driver.network_traffic.length).to eq(4) + end + + it "gets cleared when being cleared" do + @session.visit("/cuprite/with_js") + expect(@driver.network_traffic.length).to eq(4) + + @driver.clear_network_traffic + + expect(@driver.network_traffic.length).to eq(0) + end + + it "blocked requests get cleared along with network traffic" do + @driver.browser.url_blacklist = ["unwanted"] + + @session.visit "/cuprite/url_blacklist" + + expect(@driver.network_traffic(:blocked).length).to eq(2) + + @driver.clear_network_traffic + + expect(@driver.network_traffic(:blocked).length).to eq(0) + end + end + + context "memory cache clearing" do + before do + @driver.restart + end + + it "can clear memory cache when supported (>=2.0.0)" do + @driver.clear_memory_cache + + @session.visit("/cuprite/cacheable") + first_request = @driver.network_traffic.last + expect(@driver.network_traffic.length).to eq(1) + expect(first_request.response_parts.last.status).to eq(200) + + @session.visit("/cuprite/cacheable") + expect(@driver.network_traffic.length).to eq(1) + + @driver.clear_memory_cache + + @session.visit("/cuprite/cacheable") + another_request = @driver.network_traffic.last + expect(@driver.network_traffic.length).to eq(2) + expect(another_request.response_parts.last.status).to eq(200) + end + end + + context "status code support" do + it "determines status from the simple response" do + @session.visit("/cuprite/status/500") + expect(@driver.status_code).to eq(500) + end + + it "determines status code when the page has a few resources" do + @session.visit("/cuprite/with_different_resources") + expect(@driver.status_code).to eq(200) + end + + it "determines status code even after redirect" do + @session.visit("/cuprite/redirect") + expect(@driver.status_code).to eq(200) + end + end + + context "cookies support" do + it "returns set cookies" do + @session.visit("/set_cookie") + + cookie = @driver.cookies["capybara"] + expect(cookie.name).to eq("capybara") + expect(cookie.value).to eq("test_cookie") + expect(cookie.domain).to eq("127.0.0.1") + expect(cookie.path).to eq("/") + expect(cookie.secure?).to be false + expect(cookie.httponly?).to be false + expect(cookie.samesite).to be_nil + expect(cookie.expires).to be_nil + end + + it "can set cookies" do + @driver.set_cookie "capybara", "omg" + @session.visit("/get_cookie") + expect(@driver.body).to include("omg") + end + + it "can set cookies with custom settings" do + @driver.set_cookie "capybara", "omg", path: "/cuprite" + + @session.visit("/get_cookie") + expect(@driver.body).to_not include("omg") + + @session.visit("/cuprite/get_cookie") + expect(@driver.body).to include("omg") + + expect(@driver.cookies["capybara"].path).to eq("/cuprite") + end + + it "can remove a cookie" do + @session.visit("/set_cookie") + + @session.visit("/get_cookie") + expect(@driver.body).to include("test_cookie") + + @driver.remove_cookie "capybara" + + @session.visit("/get_cookie") + expect(@driver.body).to_not include("test_cookie") + end + + it "can clear cookies" do + @session.visit("/set_cookie") + + @session.visit("/get_cookie") + expect(@driver.body).to include("test_cookie") + + @driver.clear_cookies + + @session.visit("/get_cookie") + expect(@driver.body).to_not include("test_cookie") + end + + it "can set cookies with an expires time" do + time = Time.at(Time.now.to_i + 10000) + @session.visit "/" + @driver.set_cookie "foo", "bar", expires: time + expect(@driver.cookies["foo"].expires).to eq(time) + end + + it "can set cookies for given domain" do + port = @session.server.port + @driver.set_cookie "capybara", "127.0.0.1" + @driver.set_cookie "capybara", "localhost", domain: "localhost" + + @session.visit("http://localhost:#{port}/cuprite/get_cookie") + expect(@driver.body).to include("localhost") + + @session.visit("http://127.0.0.1:#{port}/cuprite/get_cookie") + expect(@driver.body).to include("127.0.0.1") + end + + it "can enable and disable cookies" do + @driver.cookies_enabled = false + @session.visit("/set_cookie") + expect(@driver.cookies).to be_empty + + @driver.cookies_enabled = true + @session.visit("/set_cookie") + expect(@driver.cookies).to_not be_empty + end + + it "sets cookies correctly when Capybara.app_host is set" do + old_app_host = Capybara.app_host + begin + Capybara.app_host = "http://localhost/cuprite" + @driver.set_cookie "capybara", "app_host" + + port = @session.server.port + @session.visit("http://localhost:#{port}/cuprite/get_cookie") + expect(@driver.body).to include("app_host") + + @session.visit("http://127.0.0.1:#{port}/cuprite/get_cookie") + expect(@driver.body).not_to include("app_host") + ensure + Capybara.app_host = old_app_host + end + end + end + + it "allows the driver to have a fixed port" do + begin + driver = Capybara::Cuprite::Driver.new(@driver.app, port: 12345) + driver.visit session_url("/") + + expect { TCPServer.new("127.0.0.1", 12345) }.to raise_error(Errno::EADDRINUSE) + ensure + driver.quit + end + end + + it "allows the driver to have a custom host" do + begin + # Use custom host "pointing" to localhost, specified by BROWSER_TEST_HOST env var. + # Use /etc/hosts or iptables for this: https://superuser.com/questions/516208/how-to-change-ip-address-to-point-to-localhost + # A custom host and corresponding env var for Travis is specified in .travis.yml + # If var is unspecified, skip test + host = ENV["BROWSER_TEST_HOST"] + + skip "BROWSER_TEST_HOST not set" if host.nil? + + driver = Capybara::Cuprite::Driver.new(@driver.app, host: host, port: 12345) + driver.visit session_url("/") + + expect { TCPServer.new(host, 12345) }.to raise_error(Errno::EADDRINUSE) + ensure + driver&.quit + end + end + + it "lists the open windows" do + @session.visit "/" + + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup") + JS + + expect(@driver.window_handles).to eq(%w[0 1]) + + popup2 = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup2") + JS + end + + expect(@driver.window_handles).to eq(%w[0 1 2]) + + @session.within_window(popup2) do + expect(@session.html).to include("Test") + @session.execute_script("window.close()") + end + + sleep 0.1 + + expect(@driver.window_handles).to eq(%w[0 1]) + end + + context "a new window inherits settings" do + it "inherits size" do + @session.visit "/" + @session.current_window.resize_to(1200, 800) + new_tab = @session.open_new_window + expect(new_tab.size).to eq [1200, 800] + end + + it "inherits url_blacklist" do + @driver.browser.url_blacklist = ["unwanted"] + @session.visit "/" + new_tab = @session.open_new_window + @session.within_window(new_tab) do + @session.visit "/cuprite/url_blacklist" + expect(@session).to have_content("We are loading some unwanted action here") + @session.within_frame "framename" do + expect(@session.html).not_to include("We shouldn\"t see this.") + end + end + end + + it "inherits url_whitelist" do + @session.visit "/" + @driver.browser.url_whitelist = ["url_whitelist", "/cuprite/wanted"] + new_tab = @session.open_new_window + @session.within_window(new_tab) do + @session.visit "/cuprite/url_whitelist" + + expect(@session).to have_content("We are loading some wanted action here") + @session.within_frame "framename" do + expect(@session).to have_content("We should see this.") + end + @session.within_frame "unwantedframe" do + # make sure non whitelisted urls are blocked + expect(@session).not_to have_content("We shouldn't see this.") + end + end + end + end + + it "resizes windows" do + @session.visit "/" + + popup1 = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup1") + JS + end + + popup2 = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup2") + JS + end + + popup1.resize_to(100, 200) + popup2.resize_to(200, 100) + + expect(popup1.size).to eq([100, 200]) + expect(popup2.size).to eq([200, 100]) + end + + it "clears local storage between tests" do + @session.visit "/" + @session.execute_script <<-JS + localStorage.setItem("key", "value"); + JS + value = @session.evaluate_script <<-JS + localStorage.getItem("key"); + JS + + expect(value).to eq("value") + + @driver.reset! + + @session.visit "/" + value = @session.evaluate_script <<-JS + localStorage.getItem("key"); + JS + expect(value).to be_nil + end + + context "basic http authentication" do + it "denies without credentials" do + @session.visit "/cuprite/basic_auth" + + expect(@session.status_code).to eq(401) + expect(@session).not_to have_content("Welcome, authenticated client") + end + + it "allows with given credentials" do + @driver.basic_authorize("login", "pass") + + @session.visit "/cuprite/basic_auth" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("Welcome, authenticated client") + end + + it "allows even overwriting headers" do + @driver.basic_authorize("login", "pass") + @driver.headers = [{ "Cuprite" => "true" }] + + @session.visit "/cuprite/basic_auth" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("Welcome, authenticated client") + end + + it "denies with wrong credentials" do + @driver.basic_authorize("user", "pass!") + + @session.visit "/cuprite/basic_auth" + + expect(@session.status_code).to eq(401) + expect(@session).not_to have_content("Welcome, authenticated client") + end + + it "allows on POST request" do + @driver.basic_authorize("login", "pass") + + @session.visit "/cuprite/basic_auth" + @session.click_button("Submit") + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("Authorized POST request") + end + end + + context "blacklisting urls for resource requests" do + it "blocks unwanted urls" do + @driver.browser.url_blacklist = ["unwanted"] + + @session.visit "/cuprite/url_blacklist" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("We are loading some unwanted action here") + @session.within_frame "framename" do + expect(@session.html).not_to include("We shouldn\"t see this.") + end + end + + it "supports wildcards" do + @driver.browser.url_blacklist = ["*wanted"] + + @session.visit "/cuprite/url_whitelist" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("We are loading some wanted action here") + @session.within_frame "framename" do + expect(@session).not_to have_content("We should see this.") + end + @session.within_frame "unwantedframe" do + expect(@session).not_to have_content("We shouldn't see this.") + end + end + + it "can be configured in the driver and survive reset" do + Capybara.register_driver :cuprite_blacklist do |app| + Capybara::Cuprite::Driver.new(app, @driver.options.merge(url_blacklist: ["unwanted"])) + end + + session = Capybara::Session.new(:cuprite_blacklist, @session.app) + + session.visit "/cuprite/url_blacklist" + expect(session).to have_content("We are loading some unwanted action here") + session.within_frame "framename" do + expect(session.html).not_to include("We shouldn't see this.") + end + + session.reset! + + session.visit "/cuprite/url_blacklist" + expect(session).to have_content("We are loading some unwanted action here") + session.within_frame "framename" do + expect(session.html).not_to include("We shouldn't see this.") + end + end + end + + context "whitelisting urls for resource requests" do + it "allows whitelisted urls" do + @driver.browser.url_whitelist = ["url_whitelist", "/wanted"] + + @session.visit "/cuprite/url_whitelist" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("We are loading some wanted action here") + @session.within_frame "framename" do + expect(@session).to have_content("We should see this.") + end + @session.within_frame "unwantedframe" do + expect(@session).not_to have_content("We shouldn't see this.") + end + end + + it "supports wildcards" do + @driver.browser.url_whitelist = ["url_whitelist", "/*wanted"] + + @session.visit "/cuprite/url_whitelist" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("We are loading some wanted action here") + @session.within_frame "framename" do + expect(@session).to have_content("We should see this.") + end + @session.within_frame "unwantedframe" do + expect(@session).to have_content("We shouldn't see this.") + end + end + + it "blocks overruled urls" do + @driver.browser.url_whitelist = ["url_whitelist"] + @driver.browser.url_blacklist = ["url_whitelist"] + + @session.visit "/cuprite/url_whitelist" + + expect(@session.status_code).to eq(nil) + expect(@session).not_to have_content("We are loading some wanted action here") + end + + it "allows urls when the whitelist is empty" do + @driver.browser.url_whitelist = [] + + @session.visit "/cuprite/url_whitelist" + + expect(@session.status_code).to eq(200) + expect(@session).to have_content("We are loading some wanted action here") + @session.within_frame "framename" do + expect(@session).to have_content("We should see this.") + end + end + + it "can be configured in the driver and survive reset" do + Capybara.register_driver :cuprite_whitelist do |app| + Capybara::Cuprite::Driver.new(app, @driver.options.merge(url_whitelist: ["url_whitelist", "/cuprite/wanted"])) + end + + session = Capybara::Session.new(:cuprite_whitelist, @session.app) + + session.visit "/cuprite/url_whitelist" + expect(session).to have_content("We are loading some wanted action here") + session.within_frame "framename" do + expect(session).to have_content("We should see this.") + end + + session.within_frame "unwantedframe" do + # make sure non whitelisted urls are blocked + expect(session).not_to have_content("We shouldn't see this.") + end + + session.reset! + + session.visit "/cuprite/url_whitelist" + expect(session).to have_content("We are loading some wanted action here") + session.within_frame "framename" do + expect(session).to have_content("We should see this.") + end + session.within_frame "unwantedframe" do + # make sure non whitelisted urls are blocked + expect(session).not_to have_content("We shouldn't see this.") + end + end + end + + context "has ability to send keys" do + before { @session.visit("/cuprite/send_keys") } + + it "sends keys to empty input" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys("Input") + + expect(input.value).to eq("Input") + end + + it "sends keys to filled input" do + input = @session.find(:css, "#filled_input") + + input.native.send_keys(" appended") + + expect(input.value).to eq("Text appended") + end + + it "sends keys to empty textarea" do + input = @session.find(:css, "#empty_textarea") + + input.native.send_keys("Input") + + expect(input.value).to eq("Input") + end + + it "sends keys to filled textarea" do + input = @session.find(:css, "#filled_textarea") + + input.native.send_keys(" appended") + + expect(input.value).to eq("Description appended") + end + + it "sends keys to empty contenteditable div" do + input = @session.find(:css, "#empty_div") + + input.native.send_keys("Input") + + expect(input.text).to eq("Input") + end + + it "persists focus across calls" do + input = @session.find(:css, "#empty_div") + + input.native.send_keys("helo") + input.native.send_keys(:Left) + input.native.send_keys("l") + + expect(input.text).to eq("hello") + end + + it "sends keys to filled contenteditable div" do + input = @session.find(:css, "#filled_div") + + input.native.send_keys(" appended") + + expect(input.text).to eq("Content appended") + end + + it "sends sequences" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys(:Shift, "S", :Alt, "t", "r", "i", "g", :Left, "n") + + expect(input.value).to eq("String") + end + + it "submits the form with sequence" do + input = @session.find(:css, "#without_submit_button input") + + input.native.send_keys(:Enter) + + expect(input.value).to eq("Submitted") + end + + it "sends sequences with modifiers and letters" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys([:Shift, "s"], "t", "r", "i", "n", "g") + + expect(input.value).to eq("String") + end + + it "sends sequences with modifiers and symbols" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys("t", "r", "i", "n", "g", %i[Ctrl Left], "s") + + expect(input.value).to eq("string") + end + + it "sends sequences with multiple modifiers and symbols" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys("t", "r", "i", "n", "g", %i[Ctrl Shift Left], "s") + + expect(input.value).to eq("s") + end + + it "sends modifiers with sequences" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys("s", [:Shift, "tring"]) + + expect(input.value).to eq("sTRING") + end + + it "sends modifiers with multiple keys" do + input = @session.find(:css, "#empty_input") + + input.native.send_keys("poltre", %i[Shift Left Left], "ergeist") + + expect(input.value).to eq("cuprite") + end + + it "has an alias" do + input = @session.find(:css, "#empty_input") + + input.native.send_key("S") + + expect(input.value).to eq("S") + end + + it "generates correct events with keyCodes for modified puncation" do + input = @session.find(:css, "#empty_input") + + input.send_keys([:shift, "."], [:shift, "t"]) + + expect(@session.find(:css, "#key-events-output")).to have_text("keydown:16 keydown:190 keydown:16 keydown:84") + end + + it "suuports snake_case sepcified keys (Capybara standard)" do + input = @session.find(:css, "#empty_input") + input.send_keys(:PageUp, :page_up) + expect(@session.find(:css, "#key-events-output")).to have_text("keydown:33", count: 2) + end + + it "supports :control alias for :Ctrl" do + input = @session.find(:css, "#empty_input") + input.send_keys([:Ctrl, "a"], [:control, "a"]) + expect(@session.find(:css, "#key-events-output")).to have_text("keydown:17 keydown:65", count: 2) + end + + it "supports :command alias for :Meta" do + input = @session.find(:css, "#empty_input") + input.send_keys([:Meta, "z"], [:command, "z"]) + expect(@session.find(:css, "#key-events-output")).to have_text("keydown:91 keydown:90", count: 2) + end + + it "supports Capybara specified numpad keys" do + input = @session.find(:css, "#empty_input") + input.send_keys(:numpad2, :numpad8, :divide, :decimal) + expect(@session.find(:css, "#key-events-output")).to have_text("keydown:98 keydown:104 keydown:111 keydown:110") + end + + it "raises error for unknown keys" do + input = @session.find(:css, "#empty_input") + expect do + input.send_keys("abc", :blah) + end.to raise_error Capybara::Cuprite::KeyError, "Unknown key: Blah" + end + end + + context "set" do + before { @session.visit("/cuprite/set") } + + it "sets a contenteditable's content" do + input = @session.find(:css, "#filled_div") + input.set("new text") + expect(input.text).to eq("new text") + end + + it "sets multiple contenteditables' content" do + input = @session.find(:css, "#empty_div") + input.set("new text") + + expect(input.text).to eq("new text") + + input = @session.find(:css, "#filled_div") + input.set("replacement text") + + expect(input.text).to eq("replacement text") + end + + it "sets a content editable childs content" do + @session.visit("/with_js") + @session.find(:css, "#existing_content_editable_child").set("WYSIWYG") + expect(@session.find(:css, "#existing_content_editable_child").text).to eq("WYSIWYG") + end + end + + context "date_fields" do + before { @session.visit("/cuprite/date_fields") } + + it "sets a date" do + input = @session.find(:css, "#date_field") + + input.set("2016-02-14") + + expect(input.value).to eq("2016-02-14") + end + + it "fills a date" do + @session.fill_in "date_field", with: "2016-02-14" + + expect(@session.find(:css, "#date_field").value).to eq("2016-02-14") + end + end + + context "evaluate_script" do + it "can return an element" do + @session.visit("/cuprite/send_keys") + element = @session.driver.evaluate_script(%(document.getElementById("empty_input"))) + expect(element).to eq(@session.find(:id, "empty_input").native) + end + + it "can return structures with elements" do + @session.visit("/cuprite/send_keys") + result = @session.driver.evaluate_script(%({ a: document.getElementById("empty_input"), b: { c: document.querySelectorAll("#empty_textarea, #filled_textarea") } })) + expect(result).to eq( + "a" => @session.driver.find_css("#empty_input").first, + "b" => { + "c" => @session.driver.find_css("#empty_textarea, #filled_textarea") + } + ) + end + end + + context "evaluate_async_script" do + it "handles evaluate_async_script value properly" do + @session.using_wait_time(5) do + expect(@session.driver.evaluate_async_script("arguments[0](null)")).to be_nil + expect(@session.driver.evaluate_async_script("arguments[0](false)")).to be false + expect(@session.driver.evaluate_async_script("arguments[0](true)")).to be true + expect(@session.driver.evaluate_async_script(%(arguments[0]({foo: "bar"})))).to eq("foo" => "bar") + end + end + + it "will timeout" do + @session.using_wait_time(1) do + expect do + @session.driver.evaluate_async_script("var callback=arguments[0]; setTimeout(function(){callback(true)}, 4000)") + end.to raise_error Capybara::Cuprite::ScriptTimeoutError + end + end + end + + context "URL" do + it "can get the frames url" do + @session.visit "/cuprite/frames" + + @session.within_frame 0 do + expect(@session.driver.frame_url).to end_with("/cuprite/slow") + if Capybara::VERSION.to_f < 3.0 + expect(@session.driver.current_url).to end_with("/cuprite/slow") + else + # current_url is required to return the top level browsing context in Capybara 3 + expect(@session.driver.current_url).to end_with("/cuprite/frames") + end + end + expect(@session.driver.frame_url).to end_with("/cuprite/frames") + expect(@session.driver.current_url).to end_with("/cuprite/frames") + end + end + end +end diff --git a/spec/integration/session_spec.rb b/spec/integration/session_spec.rb new file mode 100644 index 0000000..04d4ec2 --- /dev/null +++ b/spec/integration/session_spec.rb @@ -0,0 +1,1071 @@ +# frozen_string_literal: true + +require "spec_helper" + +# skip = [] +# skip << :windows if ENV["TRAVIS"] +# skip << :download # Browser doesn't support downloading files +# Capybara::SpecHelper.run_specs TestSessions::Cuprite, "Cuprite", capybara_skip: skip + +describe Capybara::Session do + context "with cuprite driver" do + before { @session = TestSessions::Cuprite } + + describe Capybara::Cuprite::Node do + it "raises an error if the element has been removed from the DOM" do + @session.visit("/cuprite/with_js") + node = @session.find(:css, "#remove_me") + expect(node.text).to eq("Remove me") + @session.find(:css, "#remove").click + expect { node.text }.to raise_error(Capybara::Cuprite::ObsoleteNode) + end + + it "raises an error if the element was on a previous page" do + @session.visit("/cuprite/index") + node = @session.find(".//a") + @session.execute_script "window.location = 'about:blank'" + expect { node.text }.to raise_error(Capybara::Cuprite::ObsoleteNode) + end + + it "raises an error if the element is not visible" do + @session.visit("/cuprite/index") + @session.execute_script %(document.querySelector("a[href=js_redirect]").style.display = "none") + expect { @session.click_link "JS redirect" }.to raise_error(Capybara::ElementNotFound) + end + + it "hovers an element" do + @session.visit("/cuprite/with_js") + expect(@session.find(:css, "#hidden_link span", visible: false)).to_not be_visible + @session.find(:css, "#hidden_link").hover + expect(@session.find(:css, "#hidden_link span")).to be_visible + end + + it "hovers an element before clicking it" do + @session.visit("/cuprite/with_js") + @session.click_link "Hidden link" + expect(@session.current_path).to eq("/") + end + + it "does not raise error when asserting svg elements with a count that is not what is in the dom" do + @session.visit("/cuprite/with_js") + expect { @session.has_css?("svg circle", count: 2) }.to_not raise_error + expect(@session).to_not have_css("svg circle", count: 2) + end + + context "when someone (*cough* prototype *cough*) messes with Array#toJSON" do + before do + @session.visit("/cuprite/index") + array_munge = <<-JS + Array.prototype.toJSON = function() { + return "ohai"; + } + JS + @session.execute_script array_munge + end + + it "gives a proper error" do + expect { @session.find(:css, "username") }.to raise_error(Capybara::ElementNotFound) + end + end + + context "when someone messes with JSON" do + # mootools <= 1.2.4 replaced the native JSON with it"s own JSON that didn"t have stringify or parse methods + it "works correctly" do + @session.visit("/cuprite/index") + @session.execute_script("JSON = {};") + expect { @session.find(:link, "JS redirect") }.not_to raise_error + end + end + + context "when the element is not in the viewport" do + before do + @session.visit("/cuprite/with_js") + end + + it "raises a MouseEventFailed error" do + expect { @session.click_link("O hai") }.to raise_error(Capybara::Cuprite::MouseEventFailed) + end + + context "and is then brought in" do + before do + @session.execute_script "$("#off-the-left").animate({left: "10"});" + Cuprite::SpecHelper.set_capybara_wait_time(1) + end + + it "clicks properly" do + expect { @session.click_link "O hai" }.to_not raise_error + end + + after do + Cuprite::SpecHelper.set_capybara_wait_time(0) + end + end + end + end + + context "when the element is not in the viewport of parent element" do + before do + @session.visit("/cuprite/scroll") + end + + it "scrolls into view" do + @session.click_link "Link outside viewport" + expect(@session.current_path).to eq("/") + end + + it "scrolls into view if scrollIntoViewIfNeeded fails" do + @session.click_link "Below the fold" + expect(@session.current_path).to eq("/") + end + end + + describe "Node#select" do + before do + @session.visit("/cuprite/with_js") + end + + context "when selected option is not in optgroup" do + before do + @session.find(:select, "browser").find(:option, "Firefox").select_option + end + + it "fires the focus event" do + expect(@session.find(:css, "#changes_on_focus").text).to eq("Browser") + end + + it "fire the change event" do + expect(@session.find(:css, "#changes").text).to eq("Firefox") + end + + it "fires the blur event" do + expect(@session.find(:css, "#changes_on_blur").text).to eq("Firefox") + end + + it "fires the change event with the correct target" do + expect(@session.find(:css, "#target_on_select").text).to eq("SELECT") + end + end + + context "when selected option is in optgroup" do + before do + @session.find(:select, "browser").find(:option, "Safari").select_option + end + + it "fires the focus event" do + expect(@session.find(:css, "#changes_on_focus").text).to eq("Browser") + end + + it "fire the change event" do + expect(@session.find(:css, "#changes").text).to eq("Safari") + end + + it "fires the blur event" do + expect(@session.find(:css, "#changes_on_blur").text).to eq("Safari") + end + + it "fires the change event with the correct target" do + expect(@session.find(:css, "#target_on_select").text).to eq("SELECT") + end + end + end + + describe "Node#set" do + before do + @session.visit("/cuprite/with_js") + @session.find(:css, "#change_me").set("Hello!") + end + + it "fires the change event" do + expect(@session.find(:css, "#changes").text).to eq("Hello!") + end + + it "fires the input event" do + expect(@session.find(:css, "#changes_on_input").text).to eq("Hello!") + end + + it "accepts numbers in a maxlength field" do + element = @session.find(:css, "#change_me_maxlength") + element.set 100 + expect(element.value).to eq("100") + end + + it "accepts negatives in a number field" do + element = @session.find(:css, "#change_me_number") + element.set(-100) + expect(element.value).to eq("-100") + end + + it "fires the keydown event" do + expect(@session.find(:css, "#changes_on_keydown").text).to eq("6") + end + + it "fires the keyup event" do + expect(@session.find(:css, "#changes_on_keyup").text).to eq("6") + end + + it "fires the keypress event" do + expect(@session.find(:css, "#changes_on_keypress").text).to eq("6") + end + + it "fires the focus event" do + expect(@session.find(:css, "#changes_on_focus").text).to eq("Focus") + end + + it "fires the blur event" do + expect(@session.find(:css, "#changes_on_blur").text).to eq("Blur") + end + + it "fires the keydown event before the value is updated" do + expect(@session.find(:css, "#value_on_keydown").text).to eq("Hello") + end + + it "fires the keyup event after the value is updated" do + expect(@session.find(:css, "#value_on_keyup").text).to eq("Hello!") + end + + it "clears the input before setting the new value" do + element = @session.find(:css, "#change_me") + element.set "" + expect(element.value).to eq("") + end + + it "supports special characters" do + element = @session.find(:css, "#change_me") + element.set "$52.00" + expect(element.value).to eq("$52.00") + end + + it "attaches a file when passed a Pathname" do + filename = Pathname.new("spec/tmp/a_test_pathname").expand_path + File.open(filename, "w") { |f| f.write("text") } + + element = @session.find(:css, "#change_me_file") + element.set(filename) + expect(element.value).to eq("C:\fakepath\a_test_pathname") + end + end + + describe "Node#visible" do + before do + @session.visit("/cuprite/visible") + end + + it "considers display: none to not be visible" do + expect(@session.find(:css, "li", text: "Display None", visible: false).visible?).to be false + end + + it "considers visibility: hidden to not be visible" do + expect(@session.find(:css, "li", text: "Hidden", visible: false).visible?).to be false + end + + it "considers opacity: 0 to not be visible" do + expect(@session.find(:css, "li", text: "Transparent", visible: false).visible?).to be false + end + + it "element with all children hidden returns empty text" do + expect(@session.find(:css, "div").text).to eq("") + end + end + + describe "Node#checked?" do + before do + @session.visit "/cuprite/attributes_properties" + end + + it "is a boolean" do + expect(@session.find_field("checked").checked?).to be true + expect(@session.find_field("unchecked").checked?).to be false + end + end + + describe "Node#[]" do + before do + @session.visit "/cuprite/attributes_properties" + end + + it "gets normalized href" do + expect(@session.find(:link, "Loop")["href"]).to eq("http://#{@session.server.host}:#{@session.server.port}/cuprite/attributes_properties") + end + + it "gets innerHTML" do + expect(@session.find(:css, ".some_other_class")["innerHTML"]).to eq "

foobar

" + end + + it "gets attribute" do + link = @session.find(:link, "Loop") + expect(link["data-random"]).to eq "42" + expect(link["onclick"]).to eq "return false;" + end + + it "gets boolean attributes as booleans" do + expect(@session.find_field("checked")["checked"]).to be true + expect(@session.find_field("unchecked")["checked"]).to be false + end + end + + describe "Node#==" do + it "does not equal a node from another page" do + @session.visit("/cuprite/simple") + @elem1 = @session.find(:css, "#nav") + @session.visit("/cuprite/set") + @elem2 = @session.find(:css, "#filled_div") + expect(@elem2 == @elem1).to be false + expect(@elem1 == @elem2).to be false + end + end + + it "has no trouble clicking elements when the size of a document changes" do + @session.visit("/cuprite/long_page") + @session.find(:css, "#penultimate").click + @session.execute_script <<-JS + el = document.getElementById("penultimate") + el.parentNode.removeChild(el) + JS + @session.click_link("Phasellus blandit velit") + expect(@session).to have_content("Hello") + end + + it "handles clicks where the target is in view, but the document is smaller than the viewport" do + @session.visit "/cuprite/simple" + @session.click_link "Link" + expect(@session).to have_content("Hello world") + end + + it "handles clicks where a parent element has a border" do + @session.visit "/cuprite/table" + @session.click_link "Link" + expect(@session).to have_content("Hello world") + end + + it "handles window.confirm(), returning true unconditionally" do + @session.visit "/" + expect(@session.evaluate_script(%(window.confirm("foo")))).to be true + end + + it "handles window.prompt(), returning the default value or null" do + @session.visit "/" + # Disabling because I"m not sure this is really valid + # expect(@session.evaluate_script("window.prompt()")).to be_nil + expect(@session.evaluate_script(%(window.prompt("foo", "default")))).to eq("default") + end + + it "handles evaluate_script values properly" do + expect(@session.evaluate_script("null")).to be_nil + expect(@session.evaluate_script("false")).to be false + expect(@session.evaluate_script("true")).to be true + expect(@session.evaluate_script(%({foo: "bar"}))).to eq("foo" => "bar") + end + + it "can evaluate a statement ending with a semicolon" do + expect(@session.evaluate_script("3;")).to eq(3) + end + + it "synchronises page loads properly" do + @session.visit "/cuprite/index" + @session.click_link "JS redirect" + sleep 0.1 + expect(@session.html).to include("Hello world") + end + + context "click tests" do + before do + @session.visit "/cuprite/click_test" + end + + after do + @session.driver.resize(1024, 768) + end + + it "scrolls around so that elements can be clicked" do + @session.driver.resize(200, 200) + log = @session.find(:css, "#log") + + instructions = %w[one four one two three] + instructions.each do |instruction| + @session.find(:css, "##{instruction}").click + expect(log.text).to eq(instruction) + end + end + + it "fixes some weird layout issue that we are not entirely sure about the reason for" do + @session.visit "/cuprite/datepicker" + @session.find(:css, "#datepicker").set("2012-05-11") + @session.click_link "some link" + end + + it "can click an element inside an svg" do + expect { @session.find(:css, "#myrect").click }.not_to raise_error + end + + context "with #two overlapping #one" do + before do + @session.execute_script <<-JS + var two = document.getElementById("two") + two.style.position = "absolute" + two.style.left = "0px" + two.style.top = "0px" + JS + end + + it "detects if an element is obscured when clicking" do + expect do + @session.find(:css, "#one").click + end.to raise_error(Capybara::Cuprite::MouseEventFailed) { |error| + expect(error.selector).to eq("html body div#two.box") + expect(error.message).to include("[200, 200]") + } + end + + it "clicks in the centre of an element" do + expect do + @session.find(:css, "#one").click + end.to raise_error(Capybara::Cuprite::MouseEventFailed) { |error| + expect(error.position).to eq([200, 200]) + } + end + + it "clicks in the centre of an element within the viewport, if part is outside the viewport" do + @session.driver.resize(200, 200) + + expect do + @session.find(:css, "#one").click + end.to raise_error(Capybara::Cuprite::MouseEventFailed) { |error| + expect(error.position.first).to eq(150) + } + end + end + + context "with #svg overlapping #one" do + before do + @session.execute_script <<-JS + var two = document.getElementById("svg") + two.style.position = "absolute" + two.style.left = "0px" + two.style.top = "0px" + JS + end + + it "detects if an element is obscured when clicking" do + expect do + @session.find(:css, "#one").click + end.to raise_error(Capybara::Cuprite::MouseEventFailed) { |error| + expect(error.selector).to eq("html body svg#svg.box") + expect(error.message).to include("[200, 200]") + } + end + end + + context "with image maps" do + before do + @session.visit("/cuprite/image_map") + end + + it "can click" do + @session.find(:css, "map[name=testmap] area[shape=circle]").click + expect(@session).to have_css("#log", text: "circle clicked") + @session.find(:css, "map[name=testmap] area[shape=rect]").click + expect(@session).to have_css("#log", text: "rect clicked") + end + + it "doesn't click if the associated img is hidden" do + expect do + @session.find(:css, "map[name=testmap2] area[shape=circle]").click + end.to raise_error(Capybara::ElementNotFound) + expect do + @session.find(:css, "map[name=testmap2] area[shape=circle]", visible: false).click + end.to raise_error(Capybara::Cuprite::MouseEventFailed) + end + end + end + + context "double click tests" do + before do + @session.visit "/cuprite/double_click_test" + end + + it "double clicks properly" do + @session.driver.resize(200, 200) + log = @session.find(:css, "#log") + + instructions = %w[one four one two three] + instructions.each do |instruction| + @session.find(:css, "##{instruction}").base.double_click + expect(log.text).to eq(instruction) + end + end + end + + context "status code support", status_code_support: true do + it "determines status code when an user goes to a page by using a link on it" do + @session.visit "/cuprite/with_different_resources" + + @session.click_link "Go to 500" + + expect(@session.status_code).to eq(500) + end + + it "determines properly status code when an user goes through a few pages" do + @session.visit "/cuprite/with_different_resources" + + @session.click_link "Go to 201" + @session.click_link "Do redirect" + @session.click_link "Go to 402" + + expect(@session.status_code).to eq(402) + end + end + + it "ignores cyclic structure errors in evaluate_script" do + code = <<-JS + (function() { + var a = {}; + var b = {}; + var c = {}; + c.a = a; + a.a = a; + a.b = b; + a.c = c; + return a; + })() + JS + expect(@session.evaluate_script(code)).to eq("a" => "(cyclic structure)", "b" => {}, "c" => { "a" => "(cyclic structure)" }) + end + + if Capybara::VERSION.to_f < 3.0 + it "returns BR as a space in #text" do + @session.visit "/cuprite/simple" + expect(@session.find(:css, "#break").text).to eq("Foo Bar") + end + else + it "returns BR as new line in #text" do + @session.visit "/cuprite/simple" + expect(@session.find(:css, "#break").text).to eq("Foo\nBar") + end + end + + it "handles hash changes" do + @session.visit "/#omg" + expect(@session.current_url).to match(%r{/#omg$}) + @session.execute_script <<-JS + window.onhashchange = function() { window.last_hashchange = window.location.hash } + JS + @session.visit "/#foo" + expect(@session.current_url).to match(%r{/#foo$}) + expect(@session.evaluate_script("window.last_hashchange")).to eq("#foo") + end + + context "current_url" do + let(:request_uri) { URI.parse(@session.current_url).request_uri } + + it "supports whitespace characters" do + @session.visit "/cuprite/arbitrary_path/200/foo%20bar%20baz" + expect(@session.current_path).to eq("/cuprite/arbitrary_path/200/foo%20bar%20baz") + end + + it "supports escaped characters" do + @session.visit "/cuprite/arbitrary_path/200/foo?a%5Bb%5D=c" + expect(request_uri).to eq("/cuprite/arbitrary_path/200/foo?a%5Bb%5D=c") + end + + it "supports url in parameter" do + @session.visit "/cuprite/arbitrary_path/200/foo%20asd?a=http://example.com/asd%20asd" + expect(request_uri).to eq("/cuprite/arbitrary_path/200/foo%20asd?a=http://example.com/asd%20asd") + end + + it "supports restricted characters ' []:/+&='" do + @session.visit "/cuprite/arbitrary_path/200/foo?a=%20%5B%5D%3A%2F%2B%26%3D" + expect(request_uri).to eq("/cuprite/arbitrary_path/200/foo?a=%20%5B%5D%3A%2F%2B%26%3D") + end + + it "returns about:blank when on about:blank" do + @session.visit "about:blank" + expect(@session.current_url).to eq("about:blank") + end + end + + context "dragging support" do + before do + @session.visit "/cuprite/drag" + end + + it "supports drag_to" do + draggable = @session.find(:css, "#drag_to #draggable") + droppable = @session.find(:css, "#drag_to #droppable") + + draggable.drag_to(droppable) + expect(droppable).to have_content("Dropped") + end + + it "supports drag_by on native element" do + draggable = @session.find(:css, "#drag_by .draggable") + + top_before = @session.evaluate_script(%($("#drag_by .draggable").position().top)) + left_before = @session.evaluate_script(%($("#drag_by .draggable").position().left)) + + draggable.native.drag_by(15, 15) + + top_after = @session.evaluate_script(%($("#drag_by .draggable").position().top)) + left_after = @session.evaluate_script(%($("#drag_by .draggable").position().left)) + + expect(top_after).to eq(top_before + 15) + expect(left_after).to eq(left_before + 15) + end + end + + context "window switching support" do + it "waits for the window to load" do + @session.visit "/" + + popup = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/slow", "popup") + JS + end + + @session.within_window(popup) do + expect(@session.html).to include("slow page") + end + popup.close + end + + it "can access a second window of the same name" do + @session.visit "/" + + popup = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup") + JS + end + + @session.within_window(popup) do + expect(@session.html).to include("Test") + end + popup.close + + another_popup = @session.window_opened_by do + @session.execute_script <<-JS + window.open("/cuprite/simple", "popup") + JS + end + + @session.within_window(another_popup) do + expect(@session.html).to include("Test") + end + another_popup.close + end + end + + context "frame support" do + it "supports selection by index" do + @session.visit "/cuprite/frames" + + @session.within_frame 0 do + expect(@session.driver.frame_url).to end_with("/cuprite/slow") + end + end + + it "supports selection by element" do + @session.visit "/cuprite/frames" + frame = @session.find(:css, "iframe[name]") + + @session.within_frame(frame) do + expect(@session.driver.frame_url).to end_with("/cuprite/slow") + end + end + + it "supports selection by element without name or id" do + @session.visit "/cuprite/frames" + frame = @session.find(:css, "iframe:not([name]):not([id])") + + @session.within_frame(frame) do + expect(@session.driver.frame_url).to end_with("/cuprite/headers") + end + end + + it "supports selection by element with id but no name" do + @session.visit "/cuprite/frames" + frame = @session.find(:css, "iframe[id]:not([name])") + + @session.within_frame(frame) do + expect(@session.driver.frame_url).to end_with("/cuprite/get_cookie") + end + end + + it "waits for the frame to load" do + @session.visit "/" + + @session.execute_script <<-JS + document.body.innerHTML += " + + + + + diff --git a/spec/support/views/headers.erb b/spec/support/views/headers.erb new file mode 100644 index 0000000..05e2cc5 --- /dev/null +++ b/spec/support/views/headers.erb @@ -0,0 +1,3 @@ +<% for header in request.env.select {|k,v| k.match("^HTTP.*")} %> + <%=header[0].split("_",2)[1]%>: <%=header[1]%> +<% end %> diff --git a/spec/support/views/headers_with_ajax.erb b/spec/support/views/headers_with_ajax.erb new file mode 100644 index 0000000..4377321 --- /dev/null +++ b/spec/support/views/headers_with_ajax.erb @@ -0,0 +1,25 @@ + + + + + + +
+ <% for header in request.env.select {|k,v| k.match("^HTTP.*")} %> + <%=header[0].split("_",2)[1]%>: <%=header[1]%> + <% end %> +
+
+ + diff --git a/spec/support/views/image_map.erb b/spec/support/views/image_map.erb new file mode 100644 index 0000000..9d3cc0a --- /dev/null +++ b/spec/support/views/image_map.erb @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + > + + + + +
+
+ + diff --git a/spec/support/views/index.erb b/spec/support/views/index.erb new file mode 100644 index 0000000..b99c6c1 --- /dev/null +++ b/spec/support/views/index.erb @@ -0,0 +1,6 @@ + + + + JS redirect + + diff --git a/spec/support/views/js_error.erb b/spec/support/views/js_error.erb new file mode 100644 index 0000000..181bb80 --- /dev/null +++ b/spec/support/views/js_error.erb @@ -0,0 +1,5 @@ + + +

hello

diff --git a/spec/support/views/js_redirect.erb b/spec/support/views/js_redirect.erb new file mode 100644 index 0000000..c0849ae --- /dev/null +++ b/spec/support/views/js_redirect.erb @@ -0,0 +1,8 @@ + + + + + + diff --git a/spec/support/views/long_page.erb b/spec/support/views/long_page.erb new file mode 100644 index 0000000..03a91b5 --- /dev/null +++ b/spec/support/views/long_page.erb @@ -0,0 +1,41 @@ +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lacus odio, dapibus id bibendum in, rhoncus sed dolor. In quis nulla at diam euismod suscipit vitae vitae sapien. Nam viverra hendrerit augue a accumsan. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce fermentum tortor at neque malesuada sodales. Nunc quis augue a quam venenatis pharetra sit amet et risus. Nulla pharetra enim a leo varius scelerisque aliquam urna vestibulum. Sed felis eros, iaculis convallis fermentum ac, condimentum ac lacus. Sed turpis magna, tristique eu faucibus non, faucibus vitae elit. Morbi venenatis adipiscing aliquam.

+ +

Etiam pharetra tellus eget lorem gravida sollicitudin. Curabitur malesuada pellentesque velit eu ullamcorper. In metus neque, lobortis at viverra nec, porta ac metus. Duis convallis dolor sed neque accumsan rhoncus varius elit aliquet. Sed iaculis bibendum vehicula. Duis vestibulum suscipit consequat. In mattis porttitor enim vitae sollicitudin. Phasellus condimentum dictum turpis, ac viverra neque placerat ut. Suspendisse ac arcu sed enim molestie placerat eget scelerisque quam.

+ +

Suspendisse nec nunc libero, sed gravida eros. Sed congue tellus eu purus ornare a rhoncus nisl adipiscing. Quisque pharetra est sit amet lectus vulputate imperdiet. In nec libero at tellus accumsan tristique eget eget odio. Nulla tempus, tortor at hendrerit fermentum, augue erat adipiscing dui, a lobortis risus massa non metus. Vestibulum non justo nisi, et vulputate elit. Etiam pellentesque sagittis tellus, vel tincidunt lacus consectetur vel.

+ +

Aliquam nibh metus, tincidunt a sodales vitae, accumsan sed ligula. Vivamus et ornare leo. Donec laoreet rhoncus ligula a suscipit. Nulla est sapien, varius varius adipiscing aliquet, tincidunt nec est. Donec dictum adipiscing ipsum, ac semper orci tristique at. Pellentesque suscipit odio ac enim pretium at dictum felis pharetra. Nam eget arcu arcu.

+ +

Nulla tincidunt ante vel dui imperdiet et mattis quam dictum. Pellentesque tempus, sem eu volutpat iaculis, diam urna aliquam felis, sit amet cursus ligula sem eget felis. Nullam aliquam dictum elit eu aliquam. Phasellus malesuada, quam id sodales tincidunt, magna nisl rutrum enim, sit amet varius mi elit eu tellus. Nullam venenatis, ligula eget hendrerit tincidunt, magna ligula aliquam nulla, a ullamcorper eros purus eu odio. Nunc sollicitudin porta lobortis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam euismod scelerisque porta. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc nec dui orci, ut fermentum ligula. Mauris gravida, urna ut scelerisque eleifend, lectus nisi sodales justo, dapibus egestas dui odio ut enim. Fusce a diam in felis tristique blandit. Morbi malesuada vulputate ipsum, sed ornare nisl ultricies fermentum. Mauris in enim non nulla tincidunt sollicitudin eget eget felis.

+ +

Ut malesuada varius facilisis. Fusce pharetra posuere felis a laoreet. Vivamus hendrerit risus quis eros mattis feugiat. Quisque magna sapien, pulvinar nec eleifend ac, luctus id mi. Nunc a libero mauris, vitae venenatis ante. Vestibulum eu venenatis sapien. Praesent lobortis pretium metus, id laoreet sapien tincidunt nec. Nunc condimentum nisl in nunc pharetra suscipit. Donec auctor sollicitudin nunc, nec bibendum velit tristique in. Vivamus erat velit, porttitor ut venenatis eu, hendrerit vitae nulla. Aenean tellus magna, sagittis nec rutrum at, faucibus eu lorem. Mauris malesuada eleifend convallis.

+ +

Vestibulum dignissim sapien eget arcu faucibus quis aliquet nulla rutrum. Ut ante velit, hendrerit a varius a, feugiat sed neque. Proin non neque lacinia nisi commodo varius. Nam laoreet suscipit ultrices. Suspendisse potenti. Sed ultrices posuere metus vitae viverra. Suspendisse suscipit tortor et nunc vehicula vitae venenatis leo dictum. Integer velit diam, pharetra vitae egestas eu, volutpat vestibulum nunc. Praesent odio metus, cursus nec posuere nec, tempor a sem. Integer ut est magna, eget blandit nisl. Cras at ipsum quis ante pharetra laoreet. Phasellus varius, ligula pretium pellentesque tempor, dolor mi porta tellus, ac consequat lacus ipsum id purus. Cras a cursus lorem. Nullam risus odio, auctor in vulputate ut, placerat vitae velit. Donec pulvinar scelerisque varius.

+ +

Etiam in luctus massa. Quisque adipiscing enim eu lacus aliquam vitae interdum magna malesuada. Integer dignissim ante nec elit laoreet fringilla. Morbi eu tortor feugiat dui iaculis luctus sit amet et orci. Suspendisse potenti. Aliquam pellentesque, tellus eu varius suscipit, velit eros feugiat purus, eget tincidunt nunc massa vitae sapien. Cras pellentesque tristique suscipit. Maecenas vel lorem est, non condimentum felis. In mi massa, commodo et mollis ac, aliquam in risus. Morbi malesuada aliquam malesuada. Suspendisse hendrerit mi quis nisi lacinia venenatis. Pellentesque quis dignissim ante. Suspendisse dignissim fringilla tristique. Sed eleifend tempus erat ac lacinia. Pellentesque placerat tellus sagittis velit dapibus adipiscing interdum tellus pellentesque.

+ +

Ut quis scelerisque tortor. Donec ultricies convallis arcu sed fringilla. Quisque sed mauris sem, at convallis dui. Curabitur non erat ipsum. Proin dignissim, nisl non faucibus pulvinar, nulla lacus consectetur turpis, eu vestibulum lectus ipsum rhoncus nulla. Quisque ut diam quis nibh malesuada accumsan eu non elit. Cras in lectus id odio dignissim elementum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce eget nibh arcu, at commodo lorem. Proin ac lobortis turpis. Fusce a risus dui. Vestibulum pellentesque lacinia elit, non dictum lectus luctus sit amet.

+ +

Curabitur dictum ligula et tellus luctus eget pharetra nisi lobortis. Donec eu varius libero. Curabitur lorem magna, semper at porta sed, iaculis nec lacus. In pretium erat eu ipsum dapibus pharetra. In gravida aliquet urna eget bibendum. Sed varius mi eu est sagittis vel tempor elit venenatis. Pellentesque fringilla ultricies enim sed aliquam. Nulla mattis semper erat, non rhoncus libero condimentum vel. Suspendisse eget eros commodo est malesuada accumsan. Aliquam ultricies consectetur justo, at volutpat libero pellentesque eu. Aliquam non justo id sapien mattis viverra. Morbi semper rutrum nunc dignissim blandit. Cras felis lorem, volutpat ac gravida sed, lobortis id mi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tincidunt tortor ac metus elementum dapibus vel nec nisl. Etiam at nibh nec nisi luctus bibendum vitae vel diam.

+ +

Praesent placerat nisi nec nulla ullamcorper facilisis. Nunc quis porta erat. Pellentesque et diam eu tellus aliquet porttitor a ut lorem. Nulla facilisi. Pellentesque elementum imperdiet tempus. Morbi vulputate tempor orci, quis tempor nibh pellentesque vitae. Phasellus varius ultrices diam ut faucibus. In adipiscing condimentum commodo. Sed ultrices nulla quis libero pharetra malesuada. Integer tempus volutpat sapien, elementum luctus enim bibendum non.

+ +

Sed eu quam id ligula sodales venenatis a ac leo. Fusce luctus mattis lorem, in commodo leo dictum non. Curabitur sit amet erat ut libero cursus convallis at eget mi. Nam nec turpis sit amet ligula malesuada aliquet quis id turpis. Curabitur blandit libero sed ipsum convallis semper tincidunt quam ultrices. Nulla vel neque elit, eu ornare urna. Curabitur vulputate suscipit sem quis molestie. Vestibulum nulla ipsum, porttitor varius facilisis in, laoreet in odio. Cras nec enim eget nulla mollis laoreet. Maecenas pulvinar imperdiet ante, at tristique erat luctus sit amet. Nullam convallis ornare ante in feugiat. Cras id ipsum ut dolor malesuada rutrum sit amet ut lacus. Cras nec lectus pretium lectus egestas mattis ut in ipsum.

+ +

Proin vitae massa massa, non molestie nibh. Aliquam erat volutpat. Cras in sapien purus, eget pharetra diam. Donec dignissim dictum consequat. Suspendisse varius, urna ut porta imperdiet, mauris elit egestas nibh, tempor aliquam turpis nulla ac leo. Suspendisse potenti. Vestibulum mi lorem, facilisis ultricies accumsan nec, mattis at velit. Etiam urna erat, commodo at blandit nec, eleifend nec nibh.

+ +

Donec semper felis ac nulla aliquam pulvinar. Vivamus convallis ante eget lectus mollis vitae tempor purus euismod. Praesent suscipit metus et elit egestas egestas. Donec varius fringilla mauris, ut fringilla nulla feugiat consectetur. Aenean cursus pharetra condimentum. In vestibulum, metus vel dictum aliquam, nunc lectus tempor justo, nec fermentum nulla diam eu urna. Duis ac urna neque. Nulla facilisi. Suspendisse potenti. Aenean justo diam, laoreet ut euismod id, ultricies interdum massa. Etiam adipiscing velit sed mauris ornare aliquet blandit tellus iaculis. Aliquam elementum sollicitudin euismod.

+ +

Aenean blandit lectus non augue placerat sed gravida tellus interdum. Duis vel nibh ante, nec rhoncus lorem. Sed a leo orci. Etiam a placerat orci. Aenean sit amet felis et lorem tincidunt consequat. Nulla commodo scelerisque malesuada. In ullamcorper porttitor facilisis.

+ +

Nunc congue libero vel risus dignissim posuere id ut augue. Sed sit amet metus euismod arcu porttitor ultrices. Sed et erat a elit rhoncus tincidunt. Suspendisse potenti. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque scelerisque gravida erat, sed luctus odio ultricies et. Proin imperdiet molestie leo vitae molestie. Phasellus blandit, nisi at ullamcorper vulputate, neque eros aliquam nisi, vel facilisis dui orci non lorem. Fusce malesuada lacus fringilla massa sollicitudin scelerisque. Donec rutrum lacus id mauris scelerisque dignissim. Vivamus elementum nulla sit amet ante porttitor in porttitor ipsum laoreet. Vestibulum commodo dolor id velit dictum a mollis magna hendrerit. Nunc ac neque sapien.

+ +

Nam gravida vulputate purus, nec mattis ligula congue id. Curabitur viverra magna mi. Phasellus id justo ut eros tempus placerat ut eget nisi. Aenean lectus est, aliquet eget egestas non, pellentesque vel risus. Fusce quam velit, pharetra vitae dictum at, ornare quis nisl. Duis tempor adipiscing massa, nec venenatis nulla viverra sed. Donec placerat orci in est ultricies sollicitudin vel in enim. In hac habitasse platea dictumst. Mauris et magna sapien, eget dictum nisl. Nunc id eros dui, non commodo justo. Aliquam venenatis ornare risus, ut venenatis sem imperdiet sed. Donec a odio lacus, quis fringilla neque.

+ +

Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin gravida tempor dolor, vestibulum ornare felis consequat quis. Sed suscipit diam convallis lacus egestas at ultricies nisl fermentum. Vestibulum sodales, lectus sed molestie bibendum, tellus elit luctus arcu, sit amet malesuada felis massa ac tellus. Fusce massa arcu, aliquam quis accumsan ut, ullamcorper at odio. Aenean scelerisque, lacus venenatis tincidunt commodo, urna elit mollis velit, a lacinia dui quam et turpis. Etiam ut tortor eget ipsum facilisis fermentum. Curabitur at neque sit amet nisl fringilla pulvinar eget quis elit. Quisque id magna et diam suscipit facilisis. Sed consequat volutpat lacinia.

+ +

Praesent viverra, augue vel vestibulum gravida, diam mauris sodales massa, eget cursus nisi sem in libero. Sed convallis molestie pellentesque. Ut vel ipsum massa, sit amet cursus quam. Nullam dapibus, elit eu lobortis malesuada, tellus ante consectetur diam, eget dignissim mi ante in nisl. Mauris egestas bibendum laoreet. Morbi tincidunt feugiat magna, et rutrum lectus laoreet eget. Ut ac tortor ante. In odio tortor, rhoncus a rhoncus sed, viverra ultrices metus. Quisque mollis massa velit, cursus auctor ligula. Quisque egestas arcu erat.

+ +

Phasellus blandit velit non dolor bibendum eleifend. In lobortis metus vel lorem auctor fermentum nec pulvinar nisl. Vestibulum urna mauris, malesuada quis viverra sit amet, convallis vel arcu. Mauris quis tortor ipsum, ac cursus erat. Cras laoreet accumsan elit, sed convallis nibh scelerisque vel. Praesent nec nunc dolor, et rutrum sem. Integer sagittis imperdiet arcu, et dictum nisl mattis eget. In sapien tellus, eleifend ut accumsan id, aliquam quis risus. Cras viverra neque et augue fringilla eu malesuada felis tincidunt. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer a magna non tellus fringilla gravida sed quis justo. Cras pulvinar ultricies tincidunt. Nullam hendrerit risus id massa feugiat iaculis. Duis rhoncus ipsum dolor, ut semper dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.

+
diff --git a/spec/support/views/nested_frame_test.erb b/spec/support/views/nested_frame_test.erb new file mode 100644 index 0000000..6bcc28a --- /dev/null +++ b/spec/support/views/nested_frame_test.erb @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/spec/support/views/popup_headers.erb b/spec/support/views/popup_headers.erb new file mode 100644 index 0000000..c40b155 --- /dev/null +++ b/spec/support/views/popup_headers.erb @@ -0,0 +1 @@ +pop up diff --git a/spec/support/views/requiring_custom_extension.erb b/spec/support/views/requiring_custom_extension.erb new file mode 100644 index 0000000..70c2302 --- /dev/null +++ b/spec/support/views/requiring_custom_extension.erb @@ -0,0 +1,14 @@ + + + + + +

Location:

+ + + diff --git a/spec/support/views/scroll.erb b/spec/support/views/scroll.erb new file mode 100644 index 0000000..be445af --- /dev/null +++ b/spec/support/views/scroll.erb @@ -0,0 +1,22 @@ + + + + scroll + + + +
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lacus odio, dapibus id bibendum in, rhoncus sed dolor. In quis nulla at diam euismod suscipit vitae vitae sapien. Nam viverra hendrerit augue a accumsan. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce fermentum tortor at neque malesuada sodales. Nunc quis augue a quam venenatis pharetra sit amet et risus. Nulla pharetra enim a leo varius scelerisque aliquam urna vestibulum. Sed felis eros, iaculis convallis fermentum ac, condimentum ac lacus. Sed turpis magna, tristique eu faucibus non, faucibus vitae elit. Morbi venenatis adipiscing aliquam.

+
+ +

+ Link outside viewport +

+
+
+ +
Below the fold
+
+ + diff --git a/spec/support/views/send_keys.erb b/spec/support/views/send_keys.erb new file mode 100644 index 0000000..bdad2cc --- /dev/null +++ b/spec/support/views/send_keys.erb @@ -0,0 +1,35 @@ + + + + + + + + + + +
+
Content
+ +
+ +
+
+ + + diff --git a/spec/support/views/set.erb b/spec/support/views/set.erb new file mode 100644 index 0000000..1e9289e --- /dev/null +++ b/spec/support/views/set.erb @@ -0,0 +1,10 @@ + + + + + + +
+
Content
+ + diff --git a/spec/support/views/simple.erb b/spec/support/views/simple.erb new file mode 100644 index 0000000..fd35387 --- /dev/null +++ b/spec/support/views/simple.erb @@ -0,0 +1,15 @@ + + + + Test + + + + + Link + +

Foo
Bar

+ + diff --git a/spec/support/views/svg_test.erb b/spec/support/views/svg_test.erb new file mode 100644 index 0000000..6e651f2 --- /dev/null +++ b/spec/support/views/svg_test.erb @@ -0,0 +1,12 @@ + + + + + + + svg foo + + + + + diff --git a/spec/support/views/table.erb b/spec/support/views/table.erb new file mode 100644 index 0000000..bbe51bb --- /dev/null +++ b/spec/support/views/table.erb @@ -0,0 +1,7 @@ + + + + +
+ Link +
diff --git a/spec/support/views/unwanted.erb b/spec/support/views/unwanted.erb new file mode 100644 index 0000000..dd4baa8 --- /dev/null +++ b/spec/support/views/unwanted.erb @@ -0,0 +1,6 @@ + + + + We shouldn"t see this. + + diff --git a/spec/support/views/url_blacklist.erb b/spec/support/views/url_blacklist.erb new file mode 100644 index 0000000..ac1531a --- /dev/null +++ b/spec/support/views/url_blacklist.erb @@ -0,0 +1,9 @@ + + + + We are loading some unwanted action here. + + + + + +

Here

+ + + diff --git a/spec/support/views/with_different_resources.erb b/spec/support/views/with_different_resources.erb new file mode 100644 index 0000000..b8b91fd --- /dev/null +++ b/spec/support/views/with_different_resources.erb @@ -0,0 +1,15 @@ + + + + cuprite with_different_resources + + + + + + Do redirect + Go to 201 + Go to 402 + Go to 500 + + diff --git a/spec/support/views/with_js.erb b/spec/support/views/with_js.erb new file mode 100644 index 0000000..61f9d37 --- /dev/null +++ b/spec/support/views/with_js.erb @@ -0,0 +1,75 @@ + + + + cuprite with_js + + + + + + + + +

Remove me

+

Remove

+ +

+

+

+

+ + +

+

+

+

+

+

+

+

+

+

+

+

+
O hai
+ Hidden link + + + + + + +

+ Open for match +

+ +

+ Open check twice +

+ + diff --git a/spec/support/views/zoom_test.erb b/spec/support/views/zoom_test.erb new file mode 100644 index 0000000..236f8bc --- /dev/null +++ b/spec/support/views/zoom_test.erb @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/spec/tmp/.gitkeep b/spec/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/spec/tmp/a_test_pathname b/spec/tmp/a_test_pathname new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/spec/tmp/a_test_pathname @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/spec/tmp/screenshot.jpeg b/spec/tmp/screenshot.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7ce4b658029c4c477a33ba28548a727bf344e9f3 GIT binary patch literal 29280 zcmeIwc{tR09KiA49L#}rPK?Gej+`k>V&y3^qgZQELp&+OI&us-i!G%xPgWzxigo4I zu@fp=tK>dX6rrK5oJo=^M`p*K&$EB+_Gi!M`+Gir{l3rd`M$ru7r&SPD6eiMFbnFBoH(TgF$04LPA)qV0Vn*J%W`K zlA`LH3rpL(<5XBO1}8XqcvTB-Ey}5JR?{o1yNjojqejkXdF@@fJv#2vRXDlnY6D|iqXLO*Rv@c`u<3~za=C*)>NRKX#o_AL?f~5zWcQCZi;?Uy`2< zy55-M>}RKjk(Ex{jamlJ1U^6)wO`b+)=WDpb8gb%sVn9bc__9@YJ53vGX28#ApAz0}l`Dk{0E8g$G-g^)0`@X0n4zi1q1UJy+!xjoB5?Jvo+?mqT}p zG4s?L3F4LMOc7cJ9(iu>xnf5oQyM)nZ!+(8$4mvA%KM9N4Eh+fx+AweT6=&eQAesy z?7dwy(!8|%IJwr%SWQ|XIgl3~(=M7lr`YN=Y%v_c72E@@ZvMN?t)v@K$0asf6@~U( zma9*f2$0nBx2>0}#m;1#`SmF7KO%q9K(jo;=v{;V;jwl%dJA^1b*nuq{7}`we73fw zj6EIs+(!GjGuk39I=eeBq-Um%sM|?R2#>SLN?LwdQBrW_tEU+aeuzvPaU zEwx|qr!F%NuP^s@T)&O6A@Q5UhdJqaI4`jLTv-vqeRz|)>4_Dy*YPC9zQK8oOxKHvTPp5n_74