From df804763c93f83696412797d906627b1d05b6f94 Mon Sep 17 00:00:00 2001
From: Thomas Walpole
Date: Wed, 20 Jun 2018 11:43:21 -0700
Subject: [PATCH] Add has_style? and associated matchers/assertions
---
History.md | 1 +
lib/capybara.rb | 1 +
lib/capybara/minitest.rb | 8 +++-
lib/capybara/minitest/spec.rb | 9 +++-
lib/capybara/node/element.rb | 12 +++---
lib/capybara/node/matchers.rb | 33 +++++++++++++++
lib/capybara/queries/style_query.rb | 41 +++++++++++++++++++
lib/capybara/rack_test/node.rb | 2 +-
lib/capybara/rspec/matchers.rb | 24 +++++++++++
lib/capybara/selenium/node.rb | 5 +--
lib/capybara/spec/public/test.js | 5 +++
.../spec/session/assert_style_spec.rb | 26 ++++++++++++
lib/capybara/spec/session/has_style_spec.rb | 25 +++++++++++
lib/capybara/spec/session/node_spec.rb | 6 +--
lib/capybara/spec/views/with_js.erb | 4 ++
spec/minitest_spec.rb | 8 +++-
spec/minitest_spec_spec.rb | 8 +++-
17 files changed, 201 insertions(+), 17 deletions(-)
create mode 100644 lib/capybara/queries/style_query.rb
create mode 100644 lib/capybara/spec/session/assert_style_spec.rb
create mode 100644 lib/capybara/spec/session/has_style_spec.rb
diff --git a/History.md b/History.md
index 54f00219..ebbb1987 100644
--- a/History.md
+++ b/History.md
@@ -11,6 +11,7 @@ Release date: unreleased
* `execute_async_script` can now be called on elements to run the JS in the context of the element
* `:download` filter option on `:link' selector
* Window#fullscreen
+* `Element#style` and associated matchers
### Fixes
diff --git a/lib/capybara.rb b/lib/capybara.rb
index f5111700..211818a0 100644
--- a/lib/capybara.rb
+++ b/lib/capybara.rb
@@ -406,6 +406,7 @@ module Capybara
require 'capybara/queries/match_query'
require 'capybara/queries/ancestor_query'
require 'capybara/queries/sibling_query'
+ require 'capybara/queries/style_query'
require 'capybara/node/finders'
require 'capybara/node/matchers'
diff --git a/lib/capybara/minitest.rb b/lib/capybara/minitest.rb
index 79c93606..93f26587 100644
--- a/lib/capybara/minitest.rb
+++ b/lib/capybara/minitest.rb
@@ -81,9 +81,15 @@ module Capybara
# @!method assert_xpath
# see {Capybara::Node::Matchers#assert_not_matches_selector}
+ ## Assert element has the provided CSS styles
+ #
+ # @!method assert_style
+ # see {Capybara::Node::Matchers#assert_style}
+
%w[assert_selector assert_no_selector
assert_all_of_selectors assert_none_of_selectors
- assert_matches_selector assert_not_matches_selector].each do |assertion_name|
+ assert_matches_selector assert_not_matches_selector
+ assert_style].each do |assertion_name|
class_eval <<-ASSERTION, __FILE__, __LINE__ + 1
def #{assertion_name} *args, &optional_filter_block
self.assertions +=1
diff --git a/lib/capybara/minitest/spec.rb b/lib/capybara/minitest/spec.rb
index 8076e250..a8037c1d 100644
--- a/lib/capybara/minitest/spec.rb
+++ b/lib/capybara/minitest/spec.rb
@@ -15,7 +15,8 @@ module Capybara
[%W[assert_#{assertion} must_have_#{assertion}],
%W[refute_#{assertion} wont_have_#{assertion}]]
end + [%w[assert_all_of_selectors must_have_all_of_selectors],
- %w[assert_none_of_selectors must_have_none_of_selectors]] +
+ %w[assert_none_of_selectors must_have_none_of_selectors],
+ %w[assert_style must_have_style]] +
%w[selector xpath css].flat_map do |assertion|
[%W[assert_matches_#{assertion} must_match_#{assertion}],
%W[refute_matches_#{assertion} wont_match_#{assertion}]]
@@ -163,6 +164,12 @@ module Capybara
#
# @!method wont_have_current_path
# see {Capybara::SessionMatchers#assert_no_current_path}
+
+ ##
+ # Expectation that element has style
+ #
+ # @!method must_have_style
+ # see {Capybara::SessionMatchers#assert_style}
end
end
end
diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb
index 3d49e050..b9adf56f 100644
--- a/lib/capybara/node/element.rb
+++ b/lib/capybara/node/element.rb
@@ -75,12 +75,15 @@ module Capybara
#
# Retrieve the given CSS styles
#
- # element.style('color') # => Computed value of CSS 'color' style
+ # element.style('color', 'font-size') # => Computed values of CSS 'color' and 'font-size' styles
+ #
+ # @param [String] Names of the desired CSS properties
+ # @return [Hash] Hash of the CSS property names to computed values
#
def style(*styles)
styles = styles.flatten.map(&:to_s)
raise ArgumentError, "You must specify at least one CSS style" if styles.empty?
- result = begin
+ begin
synchronize { base.style(styles) }
rescue NotImplementedError => e
begin
@@ -89,7 +92,6 @@ module Capybara
raise e
end
end
- styles.length == 1 ? result[styles[0]] : result
end
##
@@ -441,8 +443,6 @@ module Capybara
%(Obsolete #)
end
- private
-
STYLE_SCRIPT = <<~JS
(function(){
var s = window.getComputedStyle(this);
@@ -453,7 +453,7 @@ module Capybara
}
return result;
}).apply(this, arguments)
- JS
+ JS
end
end
end
diff --git a/lib/capybara/node/matchers.rb b/lib/capybara/node/matchers.rb
index 960bdda2..4cb4e244 100644
--- a/lib/capybara/node/matchers.rb
+++ b/lib/capybara/node/matchers.rb
@@ -56,6 +56,21 @@ module Capybara
false
end
+ ##
+ #
+ # Checks if a an element has the specified CSS styles
+ #
+ # element.has_style?( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
+ #
+ # @param styles [Hash]
+ # @return [Boolean] If the styles match
+ #
+ def has_style?(styles, **options)
+ assert_style(styles, **options)
+ rescue Capybara::ExpectationNotMet
+ false
+ end
+
##
#
# Asserts that a given selector is on the page or a descendant of the current node.
@@ -97,6 +112,24 @@ module Capybara
end
end
+ ##
+ #
+ # Asserts that an element has the specified CSS styles
+ #
+ # element.assert_style( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
+ #
+ # @param styles [Hash]
+ # @raise [Capybara::ExpectationNotMet] If the element doesn't have the specified styles
+ #
+ def assert_style(styles, **options)
+ query_args = _set_query_session_options(styles, options)
+ query = Capybara::Queries::StyleQuery.new(*query_args)
+ synchronize(query.wait) do
+ raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
+ end
+ true
+ end
+
# Asserts that all of the provided selectors are present on the given page
# or descendants of the current node. If options are provided, the assertion
# will check that each locator is present with those options as well (other than :wait).
diff --git a/lib/capybara/queries/style_query.rb b/lib/capybara/queries/style_query.rb
new file mode 100644
index 00000000..31495201
--- /dev/null
+++ b/lib/capybara/queries/style_query.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Capybara
+ # @api private
+ module Queries
+ class StyleQuery < BaseQuery
+ def initialize(expected_styles, session_options:, **options)
+ @expected_styles = expected_styles.each_with_object({}) { |(style, value), str_keys| str_keys[style.to_s] = value }
+ @options = options
+ @actual_styles = {}
+ super(@options)
+ self.session_options = session_options
+
+ assert_valid_keys
+ end
+
+ def resolves_for?(node)
+ @node = node
+ @actual_styles = node.style(*@expected_styles.keys)
+ @expected_styles.all? do |style, value|
+ if value.is_a? Regexp
+ @actual_styles[style] =~ value
+ else
+ @actual_styles[style] == value
+ end
+ end
+ end
+
+ def failure_message
+ +"Expected node to have styles #{@expected_styles.inspect}. " \
+ "Actual styles were #{@actual_styles.inspect}"
+ end
+
+ private
+
+ def valid_keys
+ %i[wait]
+ end
+ end
+ end
+end
diff --git a/lib/capybara/rack_test/node.rb b/lib/capybara/rack_test/node.rb
index 9c89c1f4..ba405c19 100644
--- a/lib/capybara/rack_test/node.rb
+++ b/lib/capybara/rack_test/node.rb
@@ -24,7 +24,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
string_node[name]
end
- def style(styles)
+ def style(_styles)
raise NotImplementedError, "The rack_test driver does not process CSS"
end
diff --git a/lib/capybara/rspec/matchers.rb b/lib/capybara/rspec/matchers.rb
index 5dc64cdd..8204574d 100644
--- a/lib/capybara/rspec/matchers.rb
+++ b/lib/capybara/rspec/matchers.rb
@@ -225,6 +225,24 @@ module Capybara
end
end
+ class HaveStyle < Matcher
+ def initialize(*args)
+ @args = args
+ end
+
+ def matches?(actual)
+ wrap_matches?(actual) { |el| el.assert_style(*@args) }
+ end
+
+ def does_not_match?(_actual)
+ raise ArgumentError, "The have_style matcher does not support use with not_to/should_not"
+ end
+
+ def description
+ "have style"
+ end
+ end
+
class BecomeClosed
def initialize(options)
@options = options
@@ -355,6 +373,12 @@ module Capybara
HaveSelector.new(:table, locator, options, &optional_filter_block)
end
+ # RSpec matcher for element style
+ # See {Capybara::Node::Matchers#has_style?}
+ def have_style(styles, **options)
+ HaveStyle.new(styles, options)
+ end
+
%w[selector css xpath text title current_path link button field checked_field unchecked_field select table].each do |matcher_type|
define_method "have_no_#{matcher_type}" do |*args, &optional_filter_block|
NegatedMatcher.new(send("have_#{matcher_type}", *args, &optional_filter_block))
diff --git a/lib/capybara/selenium/node.rb b/lib/capybara/selenium/node.rb
index 3b1e9bb3..d22d8710 100644
--- a/lib/capybara/selenium/node.rb
+++ b/lib/capybara/selenium/node.rb
@@ -29,9 +29,8 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
end
def style(styles)
- styles.inject({}) do |memo, style|
- memo[style] = native.css_value(style)
- memo
+ styles.each_with_object({}) do |style, result|
+ result[style] = native.css_value(style)
end
end
diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js
index e9703211..099cd0ed 100644
--- a/lib/capybara/spec/public/test.js
+++ b/lib/capybara/spec/public/test.js
@@ -63,6 +63,11 @@ $(function() {
$('title').text('changed title')
}, 400)
});
+ $('#change-size').click(function() {
+ setTimeout(function() {
+ document.getElementById('change').style.fontSize = '50px';
+ }, 500)
+ });
$('#click-test').on({
click: function(e) {
var desc = "";
diff --git a/lib/capybara/spec/session/assert_style_spec.rb b/lib/capybara/spec/session/assert_style_spec.rb
new file mode 100644
index 00000000..06236ff2
--- /dev/null
+++ b/lib/capybara/spec/session/assert_style_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+Capybara::SpecHelper.spec '#assert_style', requires: [:css] do
+ it "should not raise if the elements style contains the given properties" do
+ @session.visit('/with_html')
+ expect do
+ @session.find(:css, '#first').assert_style(display: 'block')
+ end.not_to raise_error
+ end
+
+ it "should raise error if the elements style doesn't contain the given properties" do
+ @session.visit('/with_html')
+ expect do
+ @session.find(:css, '#first').assert_style(display: 'inline')
+ end.to raise_error(Capybara::ExpectationNotMet, 'Expected node to have styles {"display"=>"inline"}. Actual styles were {"display"=>"block"}')
+ end
+
+ it "should wait for style", requires: %i[css js] do
+ @session.visit('/with_js')
+ el = @session.find(:css, '#change')
+ @session.click_link("Change size")
+ expect do
+ el.assert_style({ 'font-size': '50px' }, wait: 3)
+ end.not_to raise_error
+ end
+end
diff --git a/lib/capybara/spec/session/has_style_spec.rb b/lib/capybara/spec/session/has_style_spec.rb
new file mode 100644
index 00000000..5d2ebb9a
--- /dev/null
+++ b/lib/capybara/spec/session/has_style_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+Capybara::SpecHelper.spec '#has_style?', requires: [:css] do
+ before do
+ @session.visit('/with_html')
+ end
+
+ it "should be true if the element has the given style" do
+ expect(@session.find(:css, '#first')).to have_style(display: 'block')
+ expect(@session.find(:css, '#first').has_style?(display: 'block')).to be true
+ expect(@session.find(:css, '#second')).to have_style('display' => 'inline')
+ expect(@session.find(:css, '#second').has_style?('display' => 'inline')).to be true
+ end
+
+ it "should be false if the element does not have the given style" do
+ expect(@session.find(:css, '#first').has_style?('display' => 'inline')).to be false
+ expect(@session.find(:css, '#second').has_style?(display: 'block')).to be false
+ end
+
+ it "allows Regexp for value matching" do
+ expect(@session.find(:css, '#first')).to have_style(display: /^bl/)
+ expect(@session.find(:css, '#first').has_style?('display' => /^bl/)).to be true
+ expect(@session.find(:css, '#first').has_style?(display: /^in/)).to be false
+ end
+end
diff --git a/lib/capybara/spec/session/node_spec.rb b/lib/capybara/spec/session/node_spec.rb
index 0f259a4c..009e45a2 100644
--- a/lib/capybara/spec/session/node_spec.rb
+++ b/lib/capybara/spec/session/node_spec.rb
@@ -53,12 +53,12 @@ Capybara::SpecHelper.spec "node" do
describe "#style", requires: [:css] do
it "should return the computed style value" do
- expect(@session.find(:css, '#first').style('display')).to eq 'block'
- expect(@session.find(:css, '#second').style(:display)).to eq 'inline'
+ expect(@session.find(:css, '#first').style('display')).to eq('display' => 'block')
+ expect(@session.find(:css, '#second').style(:display)).to eq('display' => 'inline')
end
it "should return multiple style values" do
- expect(@session.find(:css, '#first').style('display', :'line-height')).to eq({ 'display' => 'block', 'line-height' => '25px' })
+ expect(@session.find(:css, '#first').style('display', :'line-height')).to eq('display' => 'block', 'line-height' => '25px')
end
end
diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb
index fefae4e7..9ea68a7b 100644
--- a/lib/capybara/spec/views/with_js.erb
+++ b/lib/capybara/spec/views/with_js.erb
@@ -72,6 +72,10 @@
Change title
+
+ Change size
+
+
Click me
diff --git a/spec/minitest_spec.rb b/spec/minitest_spec.rb
index 40d3671b..ce8ab710 100644
--- a/spec/minitest_spec.rb
+++ b/spec/minitest_spec.rb
@@ -112,6 +112,12 @@ class MinitestTest < Minitest::Test
assert_matches_xpath(find(:select, 'form_title'), './/select[@id="form_title"]')
refute_matches_xpath(find(:select, 'form_title'), './/select[@id="form_other_title"]')
end
+
+ def test_assert_style
+ skip "Rack test doesn't support style" if Capybara.current_driver == :rack_test
+ visit('/with_html')
+ assert_style(find(:css, '#second'), display: 'inline')
+ end
end
RSpec.describe 'capybara/minitest' do
@@ -126,6 +132,6 @@ RSpec.describe 'capybara/minitest' do
reporter.start
MinitestTest.run reporter, {}
reporter.report
- expect(output.string).to include("17 runs, 44 assertions, 0 failures, 0 errors, 0 skips")
+ expect(output.string).to include("18 runs, 44 assertions, 0 failures, 0 errors, 1 skips")
end
end
diff --git a/spec/minitest_spec_spec.rb b/spec/minitest_spec_spec.rb
index c21fd881..5edc1e9e 100644
--- a/spec/minitest_spec_spec.rb
+++ b/spec/minitest_spec_spec.rb
@@ -116,6 +116,12 @@ class MinitestSpecTest < Minitest::Spec
it "handles failures" do
page.must_have_select('non_existing_form_title')
end
+
+ it "supports style expectations" do
+ skip "Rack test doesn't support style" if Capybara.current_driver == :rack_test
+ visit('/with_html')
+ find(:css, '#second').must_have_style('display' => 'inline')
+ end
end
RSpec.describe 'capybara/minitest/spec' do
@@ -130,7 +136,7 @@ RSpec.describe 'capybara/minitest/spec' do
reporter.start
MinitestSpecTest.run reporter, {}
reporter.report
- expect(output.string).to include("18 runs, 41 assertions, 1 failures, 0 errors, 0 skips")
+ expect(output.string).to include("19 runs, 41 assertions, 1 failures, 0 errors, 1 skips")
# Make sure error messages are displayed
expect(output.string).to include('expected to find visible select box "non_existing_form_title" that is not disabled but there were no matches')
end