I have a custom Capybara selector
module Selectors
Capybara.add_selector(:dataAttribute) do
xpath { |attribute| ".//*[#data-target='#{attribute}']" }
end
end
find(:dataAttribute, 'myButton')
And it works just fine.
Now I need to generalize it and be able to pass also the data attribute so I can find for instance <div data-behavior"hideYourself">...</div>.
Ideally, I'd like to have the API below
find(:dataAttribute, attribute: 'behavior', value: 'hideYourself')
#OR
find(:dataAttribute, { attribute: 'behavior', value: 'hideYourself' })
So, I have updated my selector as follows
module Selectors
Capybara.add_selector(:dataAttribute) do
xpath { |params| ".//*[#data-#{params[:attribute]}='#{params[:value]}']" }
end
end
But I get NoMethodError: undefined method '[]' for nil:NilClass.
I have done a bit of debugging and I noticed in the current selector (1 string parameter) the value in the block is correctly set (myButton for instance). However, when I pass the hash the value is nil.
Any idea how to pass multiple params to a selector?
find(:dataAttribute, 'behavior', 'hideYourself') may be fine too.
Capybaras find takes the selector type, an optional locator, and then options. The problem you're running into is if you pass a Hash as a second parameter and then no third parameter that is interpreted as no locator passed and an options hash. Because of this you could use your selector as written by passing an empty options hash so the attribute/value hash is interpreted as the locator
find(:dataAttribute, attribute: 'behavior', value: 'hideYourself', {})
This could all then be wrapped up in a find_data_attribute helper method so you wouldn't have to manually pass the empty options hash.
def find_data_attribute(locator, **options)
find(:dataAttribute, locator, options)
end
Another option would be write your selector differently and use options instead - something like
Capybara.add_selector(:dataAttribute) do
xpath(:attribute, :value) do |_locator, attribute:, value:, **|
".//*[#data-#{attribute}='#{value}']"
end
end
which tells the selector to expect :attribute and :value options to be passed in which would allow find(:dataAttribute, attribute: 'behavior', value: 'hideYourself') to work.
A final option is to use a wildcard option matcher along the lines of
Capybara.add_selector(:dataAttribute) do
xpath do |_locator, **|
#you could use the locator to limit by element type if wanted - see Capybaras built-in :element selector - https://github.com/teamcapybara/capybara/blob/master/lib/capybara/selector.rb#L467
XPath.descendant
end
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
xpath[XPath.attr("data-#{name}")==val]
end
end
Which should then allow you to do
find(:dataAttribute, behavior: 'hideYourself')
to find by one data attribute or
find(:dataAttribute, behavior: 'hideYourself', other_data_attr_name: 'some value')
to find by multiples
Related
I have the following example ruby code:
def example_method(obj)
ExampleClass.new(color1: #theme.try(obj['color1'].to_sym))
end
Which should pass either the obj['color1'] as a symbol or nil to the class.
However if color1 is not passed I get the error: NoMethodError: undefined method 'to_sym' for nil:NilClass.
Shouldn't the try method be handling the exception?
Update: based on comments... I solved it by doing a ternary:
ExampleClass.new(color1: obj['color1'].present? ? #brand_theme.try(obj['color1'].try(:to_sym)) : nil)
You could write a helper method:
def theme_color(name)
return unless name
return unless #theme.respond_to?(name)
#theme.public_send(name)
end
def example_method(obj)
ExampleClass.new(color1: theme_color(obj['color1']))
end
theme_color returns nil if the argument is nil, i.e. obj['color1']. It also returns nil if theme does not respond to the given method. Otherwise, it invokes the method specified by name.
Note that respond_to? and public_send accept either a string or a symbol, so no to_sym is needed.
You could also define the helper method as an instance method of your #theme class:
class Theme
def color(name)
return unless name
return unless respond_to?(name)
public_send(name)
end
def red
'FF0000'
end
end
#theme = Theme.new
#theme.red #=> "FF0000"
#theme.color(:red) #=> "FF0000"
#theme.color('red') #=> "FF0000"
#theme.color('green') #=> nil
#theme.color(nil) #=> nil
And invoke it via:
def example_method(obj)
ExampleClass.new(color1: #theme.color(obj['color1']))
end
Keep in mind that these approaches (using public_send or try) allow you to invoke arbitrary methods on your #theme object. It might be safer to keep the colors in a hash.
From comments:
In this example obj['color1'] would be nil. So we'd be passing nil to the try.
Yes, that's an error. You can't call a method with no name. Technically, you could avoid the error by doing .try(obj['color'].to_s), but it's super-wrong.
I would check for presence explicitly and bail early if it's not there.
def example_method(obj)
return unless obj['color1'].present?
ExampleClass.new(color1: #theme.try(obj['color1']))
end
The try is called on the #theme object. The nil error is thrown because obj['color1'] returns nil and then to_sym is called on nil.
You'd have to alter the code to
ExampleClass.new(color1: #theme.try(obj['color1'].try(:to_sym) || ''))
to catch that.
And then you would have to prettify the code.
How the prettification works will depend on the use case, so I can only offer some general pointer. One way would be to have a default value to avoid having to deal with the null object
Instead of passing around nil, one simply returns a default value:
color_key = obj.fetch('color') { 'default_color' }.to_sym
ExampleClass.new(color1: #theme.send(color_key)))
This makes use of the fetch method which enables returning a default value. That way you will always have a value defined.
I'm playing around with Netflix's Workflowable gem. Right now I'm working on making a custom action where the user can choose choices.
I end up pulling {"id":1,"value":"High"} out with #options[:priority][:value]
What I want to do is get the id value of 1. Any idea how to pull that out? I tried #options[:priority][:value][:id] but that seems to through an error.
Here's what the action looks like/how I'm logging the value:
class Workflowable::Actions::UpdateStatusAction < Workflowable::Actions::Action
include ERB::Util
include Rails.application.routes.url_helpers
NAME="Update Status Action"
OPTIONS = {
:priority => {
:description=>"Enter priority to set result to",
:required=>true,
:type=>:choice,
:choices=>[{id: 1, value: "High"} ]
}
}
def run
Rails.logger.debug #options[:priority][:value]
end
end
Here's the error:
Error (3a7b2168-6f24-4837-9221-376b98e6e887): TypeError in ResultsController#flag
no implicit conversion of Symbol into Integer
Here's what #options[:priority] looks like:
{"description"=>"Enter priority to set result to", "required"=>true, "type"=>:choice, "choices"=>[{"id"=>1, "value"=>"High"}], "value"=>"{\"id\":1,\"value\":\"High\"}", "user_specified"=>true}
#options[:priority]["value"] looks to be a strong containing json, not a hash. This is why you get an error when using [:id] (this method doesn't accept symbols) and why ["id"] returns the string "id".
You'll need to parse it first, for example with JSON.parse, at which point you'll have a hash which you should be able to access as normal. By default the keys will be strings so you'll need
JSON.parse(value)["id"]
I'm assuming the error is something like TypeError: no implicit conversion of Symbol into Integer
It looks like #options[:priority] is a hash with keys :id and :value. So you would want to use #options[:priority][:id] (lose the :value that returns the string).
I'm working on a method that will allow me to add in a "word" and its "definition", into a hash.
Here's what I have:
class Dictionary
def entries
#entries ||= {}
end
def add word, definition = nil
entries[word] = definition
"#{entries}"
end
end
Note: I want the definition parameter to be optional, hence my initialization to nil. However, for some reason that is showing up in my output.
Example: Passing in "fish" and "aquatic animal":
My output: {{"fish"=>"aquatic animal"}=>nil}
Desired output: {"fish"=>"aquatic animal"}
It seems like the problem is that it's putting both values that I pass to the method into the first key in the hash, and is putting that "nil" value into that key's value. Where am I making an error?
Edit: Adding the relevant RSpec block that is doing the method call so that I can better understand exactly how RSpec is making this call:
describe Dictionary do
before do
#d = Dictionary.new
end
it 'is empty when created' do
#d.entries.should == {}
end
it 'can add whole entries with keyword and definition' do
#d.add('fish' => 'aquatic animal')
#d.entries.should == {'fish' => 'aquatic animal'}
#d.keywords.should == ['fish']
end
Thanks!
If you want to optionally accept a hash entry...
def add word, definition = nil
if word.class == Hash
entries.merge!(word)
else
entries[word] = definition
end
"#{entries}"
end
You don't want to do
#d.add('fish' => 'aquatic animal')
You want to do...
#d.add('fish', 'aquatic animal')
As it is, you're passing a hash as the first argument, second argument is empty.
Your RSpec is wrong.
Change #d.add('fish' => 'aquatic animal') to #d.add('fish', 'aquatic animal')
Your #add method is accepting 2 parameters, with one being optional. With your current code, you're passing in a single hash 'fish' => 'aquatic animal'. Therefor setting word to the hash, and def to nil.
I am learning rails and going back to ruby to understand how methods in rails (and ruby really work). When I see method calls like:
validates :first_name, :presence => true
I get confused. How do you write methods in ruby that accept symbols or hashes. The source code for the validates method is confusing too. Could someone please simplify this topic of using symbols as arguments in ruby class and instance methods for me?
UPDATE:
Good one #Dave! But What I was trying out was something like:
def full_name (:first_name, :last_name)
#first_name = :first_name
#last_name = :last_name
p "#{#first_name} #{last_name}"
end
full_name("Breta", "Von Sustern")
Which obviously raises errors. I am trying to understand: Why is passing symbols like this as arguments wrong if symbols are just like any other value?
Symbols and hashes are values like any other, and can be passed like any other value type.
Recall that ActiveRecord models accept a hash as an argument; it ends up being similar to this (it's not this simple, but it's the same idea in the end):
class User
attr_accessor :fname, :lname
def initialize(args)
#fname = args[:fname] if args[:fname]
#lname = args[:lname] if args[:lname]
end
end
u = User.new(:fname => 'Joe', :lname => 'Hacker')
This takes advantage of not having to put the hash in curly-brackets {} unless you need to disambiguate parameters (and there's a block parsing issue as well when you skip the parens).
Similarly:
class TestItOut
attr_accessor :field_name, :validations
def initialize(field_name, validations)
#field_name = field_name
#validations = validations
end
def show_validations
puts "Validating field '#{field_name}' with:"
validations.each do |type, args|
puts " validator '#{type}' with args '#{args}'"
end
end
end
t = TestItOut.new(:name, presence: true, length: { min: 2, max: 10 })
t.show_validations
This outputs:
Validating field 'name' with:
validator 'presence' with args 'true'
validator 'length' with args '{min: 2, max: 10}'
From there you can start to see how things like this work.
I thought I'd add an update for Ruby 2+ since this is the first result I found for 'symbols as arguments'.
Since Ruby 2.0.0 you can also use symbols when defining a method. When calling the method these symbols will then act almost the same as named optional parameters in other languages. See example below:
def variable_symbol_method(arg, arg_two: "two", arg_three: "three")
[arg, arg_two, arg_three]
end
result = variable_symbol_method :custom_symbol, arg_three: "Modified symbol arg"
# result is now equal to:
[:custom_symbol, "two", "Modified symbol arg"]
As shown in the example, we omit arg_two: when calling the method and in the method body we can still access it as variable arg_two. Also note that the variable arg_three is indeed altered by the function call.
In Ruby, if you call a method with a bunch of name => value pairs at the end of the argument list, these get automatically wrapped in a Hash and passed to your method as the last argument:
def foo(kwargs)
p kwargs
end
>> foo(:abc=>"def", 123=>456)
{:abc=>"def", 123=>456}
>> foo("cabbage")
"cabbage"
>> foo(:fluff)
:fluff
There's nothing "special" about how you write the method, it's how you call it. It would be perfectly legal to just pass a regular Hash object as the kwargs parameter. This syntactic shortcut is used to implement named parameters in an API.
A Ruby symbol is just a value as any other, so in your example, :first_name is just a regular positional argument. :presence is a symbol used as a Hash key – any type can be used as a Hash key, but symbols are a common choice because they're immutable values.
I think all replies have missed the point of question; and the fact it is asked by someone who is - I guess - not clear on what a symbol is ?
As a newcomer to Ruby I had similar confusions and to me an answer like following would have made more sense
Method Arguments are local variables populated by passed in values.
You cant use symbols as Arguments by themselves, as you cant change value of a symbol.
Symbols are not limited to hashes. They are identifiers, without the extra storage space of a string. It's just a way to say "this is ...."
A possible function definition for the validates call could be (just to simplify, I don't know off the top of my head what it really is):
def validates(column, options)
puts column.to_s
if options[:presence]
puts "Found a presence option"
end
end
Notice how the first symbol is a parameter all of its own, and the rest is the hash.
When I assign in my controller
#my_hash = { :my_key => :my_value }
and test that controller by doing
get 'index'
assigns(:my_hash).should == { :my_key => :my_value }
then I get the following error message:
expected: {:my_key=>:my_value},
got: {"my_key"=>:my_value} (using ==)
Why does this automatic symbol to string conversion happen? Why does it affect the key of the hash?
It may end up as a HashWithIndifferentAccess if Rails somehow gets ahold of it, and that uses string keys internally. You might want to verify the class is the same:
assert_equal Hash, assigns(:my_hash).class
Parameters are always processed as the indifferent access kind of hash so you can retrieve using either string or symbol. If you're assigning this to your params hash on the get or post call, or you might be getting converted.
Another thing you can do is freeze it and see if anyone attempts to modify it because that should throw an exception:
#my_hash = { :my_key => :my_value }.freeze
You might try calling "stringify_keys":
assigns(:my_hash).should == { :my_key => :my_value }.stringify_keys
AHA! This is happening not because of Rails, per se, but because of Rspec.
I had the same problem testing the value of a Hashie::Mash in a controller spec (but it applies to anything that quacks like a Hash)
Specifically, in a controller spec, when you call assigns to access the instance variables set in the controller action, it's not returning exactly the instance variable you set, but rather, a copy of the variable that Rspec stores as a member of a HashWithIndifferentAccess (containing all the assigned instance variables). Unfortunately, when you stick a Hash (or anything that inherits from Hash) into a HashWithIndifferentAccess, it is automatically converted to an instance of that same, oh-so-convenient but not-quite-accurate class :)
The easiest work-around is to avoid the conversion by accessing the variable directly, before it's converted "for your convenience", using: controller.view_assigns['variable_name'] (note: the key here must be a string, not a symbol)
So the test in the original post should pass if it were changed to:
get 'index'
controller.view_assigns['my_hash'].should == { :my_key => :my_value }
(of course, .should is no longer supported in new versions of RSpec, but just for comparison I kept it the same)
See this article for further explanation:
http://ryanogles.by/rails/hashie/rspec/testing/2012/12/26/rails-controller-specs-dont-always-play-nice-with-hashie.html
I know this is old, but if you are upgrading from Rails-3 to 4, your controller tests may still have places where Hash with symbol keys was used but compared with the stringified version, just to prevent the wrong expectation.
Rails-4 has fixed this issue: https://github.com/rails/rails/pull/5082 .
I suggest updating your tests to have expectations against the actual keys.
In Rails-3 the assigns method converts your #my_hash to HashWithIndifferentAccess that stringifies all the keys -
def assigns(key = nil)
assigns = #controller.view_assigns.with_indifferent_access
key.nil? ? assigns : assigns[key]
end
https://github.com/rails/rails/blob/3-2-stable/actionpack/lib/action_dispatch/testing/test_process.rb#L7-L10
Rails-4 updated it to return the original keys -
def assigns(key = nil)
assigns = {}.with_indifferent_access
#controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) }
key.nil? ? assigns : assigns[key]
end
https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/testing/test_process.rb#L7-L11
You can also pass your Hash object to the initializer of HashWithIndifferentAccess.
You can use HashWithIndifferentAccess.new as Hash init:
Thor::CoreExt::HashWithIndifferentAccess.new( to: 'mail#somehost.com', from: 'from#host.com')