Skip to content

Commit

Permalink
Disable animation on pages
Browse files Browse the repository at this point in the history
  • Loading branch information
twalpole committed Jun 1, 2018
1 parent 15313bd commit e8b49b6
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 54 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Metrics/ModuleLength:
Metrics/PerceivedComplexity:
Enabled: false

Metrics/ParameterLists:
Enabled: false

Lint/UnusedMethodArgument:
Exclude:
- 'lib/capybara/driver/base.rb'
Expand Down
2 changes: 2 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Release date: unreleased
* `:element` selector type which will match on any attribute (other than the reserved names) passed as a filter option
* `:class` filter option now supports preceding class names with `!` to indicate not having that class
* `:class` and `:id` filter options now accept `XPath::Expression` objects to allow for more flexibility in matching
* `Capybara.disable_animation` setting which triggers loading of a middleware that attempts to disable animations in pages.
This is very much a beta feature and may change/disappear in the future. [Thomas Walpole]

# Version 3.1.1
Release date: 2018-05-25
Expand Down
2 changes: 0 additions & 2 deletions lib/capybara/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
end

# rubocop:disable Metrics/BlockLength
# rubocop:disable Metrics/ParameterLists

Capybara.add_selector(:xpath) do
xpath { |xpath| xpath }
Expand Down Expand Up @@ -457,4 +456,3 @@
end
end
# rubocop:enable Metrics/BlockLength
# rubocop:enable Metrics/ParameterLists
2 changes: 1 addition & 1 deletion lib/capybara/selector/filter_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def options_with_defaults(options)
options
end

def add_filter(name, filter_class, *types, matcher: nil, **options, &block) # rubocop:disable Metrics/ParameterLists
def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
types.each { |k| options[k] = true }
raise "ArgumentError", ":default option is not supported for filters with a :matcher option" if matcher && options[:default]
if filter_class <= Filters::ExpressionFilter
Expand Down
54 changes: 5 additions & 49 deletions lib/capybara/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,11 @@
require 'uri'
require 'net/http'
require 'rack'
require 'capybara/server/middleware'
require 'capybara/server/animation_disabler'

module Capybara
class Server
class Middleware
class Counter
attr_reader :value

def initialize
@value = 0
@mutex = Mutex.new
end

def increment
@mutex.synchronize { @value += 1 }
end

def decrement
@mutex.synchronize { @value -= 1 }
end
end

attr_accessor :error

def initialize(app, server_errors)
@app = app
@counter = Counter.new
@server_errors = server_errors
end

def pending_requests?
@counter.value.positive?
end

def call(env)
if env["PATH_INFO"] == "/__identify__"
[200, {}, [@app.object_id.to_s]]
else
@counter.increment
begin
@app.call(env)
rescue *@server_errors => e
@error ||= e
raise e
ensure
@counter.decrement
end
end
end
end

class << self
def ports
@ports ||= {}
Expand All @@ -61,9 +16,10 @@ def ports

attr_reader :app, :port, :host

def initialize(app, *deprecated_options, port: Capybara.server_port, host: Capybara.server_host, reportable_errors: Capybara.server_errors)
def initialize(app, *deprecated_options, port: Capybara.server_port, host: Capybara.server_host, reportable_errors: Capybara.server_errors, extra_middleware: [])
warn "Positional arguments, other than the application, to Server#new are deprecated, please use keyword arguments" unless deprecated_options.empty?
@app = app
@extra_middleware = extra_middleware
@server_thread = nil # suppress warnings
@host = deprecated_options[1] || host
@reportable_errors = deprecated_options[2] || reportable_errors
Expand Down Expand Up @@ -147,7 +103,7 @@ def https_connect
end

def middleware
@middleware ||= Middleware.new(app, @reportable_errors)
@middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
end

def port_key
Expand Down
43 changes: 43 additions & 0 deletions lib/capybara/server/animation_disabler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Capybara
class Server
class AnimationDisabler
def initialize(app)
@app = app
end

def call(env)
@status, @headers, @body = @app.call(env)
return [@status, @headers, @body] unless html_content?
response = Rack::Response.new([], @status, @headers)

@body.each { |html| response.write insert_disable(html) }
@body.close if @body.respond_to?(:close)

response.finish
end

private

def html_content?
!!(@headers["Content-Type"] =~ /html/)
end

def insert_disable(html)
html.sub(%r{(</head>)}, DISABLE_MARKUP + '\\1')
end

DISABLE_MARKUP = <<~HTML
<script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
<style>
* {
transition: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
HTML
end
end
end
55 changes: 55 additions & 0 deletions lib/capybara/server/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Capybara
class Server
class Middleware
class Counter
attr_reader :value

def initialize
@value = 0
@mutex = Mutex.new
end

def increment
@mutex.synchronize { @value += 1 }
end

def decrement
@mutex.synchronize { @value -= 1 }
end
end

attr_accessor :error

def initialize(app, server_errors, extra_middleware = [])
@app = app
@extended_app = extra_middleware.inject(@app) do |ex_app, klass|
klass.new(ex_app)
end
@counter = Counter.new
@server_errors = server_errors
end

def pending_requests?
@counter.value.positive?
end

def call(env)
if env["PATH_INFO"] == "/__identify__"
[200, {}, [@app.object_id.to_s]]
else
@counter.increment
begin
@extended_app.call(env)
rescue *@server_errors => e
@error ||= e
raise e
ensure
@counter.decrement
end
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/capybara/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def initialize(mode, app = nil)
yield config
end
@server = if config.run_server && @app && driver.needs_server?
Capybara::Server.new(@app, port: config.server_port, host: config.server_host, reportable_errors: config.server_errors).boot
server_options = { port: config.server_port, host: config.server_host, reportable_errors: config.server_errors }
server_options[:extra_middleware] = [Capybara::Server::AnimationDisabler] if config.disable_animation
Capybara::Server.new(@app, server_options).boot
end
@touched = false
end
Expand Down
10 changes: 9 additions & 1 deletion lib/capybara/session/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class SessionConfig
OPTIONS = %i[always_include_port run_server default_selector default_max_wait_time ignore_hidden_elements
automatic_reload match exact exact_text raise_server_errors visible_text_only
automatic_label_click enable_aria_label save_path asset_host default_host app_host
server_host server_port server_errors default_set_options].freeze
server_host server_port server_errors default_set_options disable_animation].freeze

attr_accessor(*OPTIONS)

Expand Down Expand Up @@ -52,6 +52,8 @@ class SessionConfig
# See {Capybara.configure}
# @!method default_set_options
# See {Capybara.configure}
# @!method disable_animation
# See {Capybara.configure}

remove_method :server_host

Expand Down Expand Up @@ -80,6 +82,12 @@ def default_host=(url)
@default_host = url
end

remove_method :disable_animation=
def disable_animation=(bool)
warn "Capybara.disable_animation is a beta feature - it may change/disappear in a future point version" if bool
@disable_animation = bool
end

def initialize_copy(other)
super
@server_errors = @server_errors.dup
Expand Down
1 change: 1 addition & 0 deletions lib/capybara/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def reset!
Capybara.match = :smart
Capybara.enable_aria_label = false
Capybara.default_set_options = {}
Capybara.disable_animation = false
reset_threadsafe
end

Expand Down
46 changes: 46 additions & 0 deletions lib/capybara/spec/views/with_animation.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>with_animation</title>
<script src="/jquery.js" type="text/javascript" charset="utf-8"></script>
<script src="/jquery-ui.js" type="text/javascript" charset="utf-8"></script>
<script src="/test.js" type="text/javascript" charset="utf-8"></script>
<style>
.transition.away {
width: 0%;
}

a {
display: inline-block;
width: 100%;
overflow: hidden;
}

a:not(.away) {
height: 20px;
}

a.transition {
transition: all 3s ease-in-out;
}

@keyframes animation {
0% {height: 20px; width: 100%;}
100% {height: 0px; width: 0%;}
}

a.animation.away {
animation-name: animation;
animation-duration: 3s;
animation-fill-mode: forwards;
}
</style>
</head>

<body id="with_animation">
<a href='#' class='transition' onclick='this.classList.add("away")'>transition me away</a>
<a href='#' class='animation' onclick='this.classList.add("away")'>animate me away</a>
</body>
</html>

23 changes: 23 additions & 0 deletions spec/shared_selenium_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,29 @@
end.to raise_error(ArgumentError, 'Not allowed to close the primary window')
end
end

context "AnimationDisabler" do
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
Capybara.disable_animation = true
@animation_session = Capybara::Session.new(session.mode, TestApp.new)
end

after(:context) do # rubocop:disable RSpec/BeforeAfterAll
Capybara.disable_animation = false
end

it "should disable CSS transitions" do
@animation_session.visit('with_animation')
@animation_session.click_link('transition me away')
expect(@animation_session).to have_no_link('transition me away', wait: 0.5)
end

it "should disable CSS animations", :focus_ do
@animation_session.visit('with_animation')
@animation_session.click_link('animate me away')
expect(@animation_session).to have_no_link('animate me away', wait: 0.5)
end
end
end

def headless_or_remote?
Expand Down

0 comments on commit e8b49b6

Please sign in to comment.