Skip to content

Commit

Permalink
Merge pull request teamcapybara#2055 from teamcapybara/css_style
Browse files Browse the repository at this point in the history
Add Element method for getting specific CSS styles
  • Loading branch information
twalpole authored Jun 23, 2018
2 parents 5c8ca38 + df80476 commit 8f365bd
Show file tree
Hide file tree
Showing 21 changed files with 265 additions and 7 deletions.
1 change: 1 addition & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/capybara.rb
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ module Selenium; end
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'
Expand Down
4 changes: 4 additions & 0 deletions lib/capybara/driver/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def value
raise NotImplementedError
end

def style(styles)
raise NotImplementedError
end

# @param value String or Array. Array is only allowed if node has 'multiple' attribute
# @param options [Hash{}] Driver specific options for how to set a value on a node
def set(value, **options)
Expand Down
8 changes: 7 additions & 1 deletion lib/capybara/minitest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ def #{assertion_name} *args
# @!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
Expand Down
9 changes: 8 additions & 1 deletion lib/capybara/minitest/spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ module Expectations
[%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}]]
Expand Down Expand Up @@ -163,6 +164,12 @@ def #{new_name} *args, &block
#
# @!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
Expand Down
35 changes: 35 additions & 0 deletions lib/capybara/node/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,29 @@ def [](attribute)
synchronize { base[attribute] }
end

##
#
# Retrieve the given CSS styles
#
# 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?
begin
synchronize { base.style(styles) }
rescue NotImplementedError => e
begin
evaluate_script(STYLE_SCRIPT, *styles)
rescue Capybara::NotSupportedByDriverError
raise e
end
end
end

##
#
# @return [String] The value of the form element
Expand Down Expand Up @@ -419,6 +442,18 @@ def inspect

%(Obsolete #<Capybara::Node::Element>)
end

STYLE_SCRIPT = <<~JS
(function(){
var s = window.getComputedStyle(this);
var result = {};
for (var i = arguments.length; i--; ) {
var property_name = arguments[i];
result[property_name] = s.getPropertyValue(property_name);
}
return result;
}).apply(this, arguments)
JS
end
end
end
33 changes: 33 additions & 0 deletions lib/capybara/node/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ def has_no_selector?(*args, &optional_filter_block)
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.
Expand Down Expand Up @@ -97,6 +112,24 @@ def assert_selector(*args, &optional_filter_block)
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).
Expand Down
41 changes: 41 additions & 0 deletions lib/capybara/queries/style_query.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/capybara/rack_test/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def [](name)
string_node[name]
end

def style(_styles)
raise NotImplementedError, "The rack_test driver does not process CSS"
end

def value
string_node.value
end
Expand Down
24 changes: 24 additions & 0 deletions lib/capybara/rspec/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,24 @@ def failure_message_when_negated
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
Expand Down Expand Up @@ -355,6 +373,12 @@ def have_table(locator = nil, **options, &optional_filter_block)
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))
Expand Down
6 changes: 6 additions & 0 deletions lib/capybara/selenium/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def value
end
end

def style(styles)
styles.each_with_object({}) do |style, result|
result[style] = native.css_value(style)
end
end

##
#
# Set the value of the form element to the given value.
Expand Down
5 changes: 5 additions & 0 deletions lib/capybara/spec/public/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down
26 changes: 26 additions & 0 deletions lib/capybara/spec/session/assert_style_spec.rb
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions lib/capybara/spec/session/has_style_spec.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions lib/capybara/spec/session/node_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@
end
end

describe "#style", requires: [:css] do
it "should return the computed style value" do
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')
end
end

describe "#value" do
it "should allow retrieval of the value" do
expect(@session.find('//textarea[@id="normal"]').value).to eq('banana')
Expand Down
8 changes: 6 additions & 2 deletions lib/capybara/spec/views/with_html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@

<style>
p { display: block; }
</style>

<div id="referrer"><%= referrer %></div>
<h1>This is a test</h1>

Expand All @@ -13,7 +17,7 @@
<span class="number">42</span>
<span>Other span</span>

<p class="para" id="first" data-random="abc\def">
<p class="para" id="first" data-random="abc\def" style="line-height: 25px;">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut <a href="/with_simple_html" title="awesome title" class="simple">labore</a>
et dolore magna aliqua. Ut enim ad minim veniam,
Expand All @@ -22,7 +26,7 @@
<a href="/with_simple_html" aria-label="Go to simple"><img id="first_image" width="20" height="20" alt="awesome image" /></a>
</p>

<p class="para" id="second">
<p class="para" id="second" style="display: inline;">
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat <a href="/redirect" id="red">Redirect</a> pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia
Expand Down
4 changes: 4 additions & 0 deletions lib/capybara/spec/views/with_js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
<a href="#" id="change-title">Change title</a>
</p>

<p>
<a href="#" id="change-size">Change size</a>
</p>

<p id="click-test">Click me</p>

<p>
Expand Down
2 changes: 1 addition & 1 deletion spec/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TestClass
end

Capybara::SpecHelper.run_specs TestClass.new, "DSL", capybara_skip: %i[
js modals screenshot frames windows send_keys server hover about_scheme psc download
js modals screenshot frames windows send_keys server hover about_scheme psc download css
]

RSpec.describe Capybara::DSL do
Expand Down
Loading

0 comments on commit 8f365bd

Please sign in to comment.