Disable animation on pages

This commit is contained in:
Thomas Walpole 2018-05-16 20:04:24 -07:00
parent 15313bd269
commit e8b49b6eed
12 changed files with 191 additions and 54 deletions

View File

@ -40,6 +40,9 @@ Metrics/ModuleLength:
Metrics/PerceivedComplexity:
Enabled: false
Metrics/ParameterLists:
Enabled: false
Lint/UnusedMethodArgument:
Exclude:
- 'lib/capybara/driver/base.rb'

View File

@ -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

View File

@ -24,7 +24,6 @@ Capybara::Selector::FilterSet.add(:_field) do
end
# rubocop:disable Metrics/BlockLength
# rubocop:disable Metrics/ParameterLists
Capybara.add_selector(:xpath) do
xpath { |xpath| xpath }
@ -457,4 +456,3 @@ Capybara.add_selector(:element) do
end
end
# rubocop:enable Metrics/BlockLength
# rubocop:enable Metrics/ParameterLists

View File

@ -59,7 +59,7 @@ module Capybara
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

View File

@ -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 ||= {}
@ -61,9 +16,10 @@ module Capybara
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
@ -147,7 +103,7 @@ module Capybara
end
def middleware
@middleware ||= Middleware.new(app, @reportable_errors)
@middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
end
def port_key

View File

@ -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

View File

@ -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

View File

@ -84,7 +84,9 @@ module Capybara
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

View File

@ -7,7 +7,7 @@ module Capybara
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)
@ -52,6 +52,8 @@ module Capybara
# See {Capybara.configure}
# @!method default_set_options
# See {Capybara.configure}
# @!method disable_animation
# See {Capybara.configure}
remove_method :server_host
@ -80,6 +82,12 @@ module Capybara
@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

View File

@ -30,6 +30,7 @@ module Capybara
Capybara.match = :smart
Capybara.enable_aria_label = false
Capybara.default_set_options = {}
Capybara.disable_animation = false
reset_threadsafe
end

View File

@ -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>

View File

@ -256,6 +256,29 @@ RSpec.shared_examples "Capybara::Session" do |session, mode|
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?