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