Dynamically Choosing Elememt Types with Watir - ruby

In watir we have the ability to find elements by text such as browser.div(text:'some text').
In my project there are a lot of common elements like a menu dropdown that contains divs with text and class "items". In this case I could do browser.div(text:'some text', class: 'items') if I wanted a more generic way to access those elements.
I have this same thing with other html elements like links, spans, buttons etc. There are instances where the same text is in multiple places but just different element types. Is there anyway I could do a method where I pass the element type and still find by text?
Something like
def get_element_by_text(tag, text)
browser.tag(text:'some text')
end

Yes, you can, write the following method
browser.element(tag_name: tag,text:'some text')
In my above code tag is the variable which would receive any tag name via your formal argument of the method.

In addition to Rajagopalan's answer, if you also want to differentiate between finding an array of elements or a single element, you can always user Ruby's send() function:
https://apidock.com/ruby/Object/send
tag = 'input'
browser.send(tag, text: 'some text')
# browser.input(text: 'some text')
tag = 'inputs'
browser.send(tag, text: 'some text')
# browser.inputs(text: 'some text')
So in your case:
def get_element_by_text(tag, text)
browser.send(tag, text: text)
end
Or make it even more dynamic
def get_element(tag, *args)
browser.send(tag, args)
end
Note: Personally I'm against these kind of one-funtion-to-rule-them-all kind of behaviour. Standard Watir is in a good balance of code. When doing this, before you know it you're re-writing watir.

Related

How to create and use variables dynamically named by string values in Ruby?

I'm using SitePrism to create some POM tests. One of my page classes looks like this:
class HomePage < SitePrism::Page
set_url '/index.html'
element :red_colour_cell, "div[id='colour-cell-red']"
element :green_colour_cell, "div[id='colour-cell-green']"
element :blue_colour_cell, "div[id='colour-cell-blue']"
def click_colour_cell(colour)
case colour
when 'red'
has_red_colour_cell?
red_colour_cell.click
when 'green'
has_green_colour_cell?
green_colour_cell.click
when 'blue'
has_blue_colour_cell?
blue_colour_cell.click
end
end
end
The method click_colour_cell() get its string value passed from a Capybara test step that calls this method.
If I need to create additional similar methods in the future, it can become rather tedious and unwieldy having so many case switches to determine the code flow.
Is there some way I can create a variable that is dynamically named by the string value of another variable? For example, I would like to do something for click_colour_cell() that resembles the following:
def click_colour_cell(colour)
has_#colour_colour_cell?
#colour_colour_cell.click
end
where #colour represents the value of the passed value, colour and would be interpreted by Ruby:
def click_colour_cell('blue')
has_blue_colour_cell?
blue_colour_cell.click
end
Isn't this what instance variables are used for? I've tried the above proposal as a solution, but I receive the ambiguous error:
syntax error, unexpected end, expecting ':'
end
^~~ (SyntaxError)
If it is an instance variable that I need to use, then I'm not sure I'm using it correctly. if it's something else I need to use, please advise.
Instance variables are used define properties of an object.
Instead you can achieve through the method send and string interpolation.
Try the below:
def click_colour_cell(colour)
send("has_#{colour}_colour_cell?")
send("#{colour}_colour_cell").click
end
About Send:
send is the method defined in the Object class (parent class for all the classes).
As the documentation says, it invokes the method identified by the given String or Symbol. You can also pass arguments to the methods you are trying to invoke.
On the below snippet, send will search for a method named testing and invokes it.
class SendTest
def testing
puts 'Hey there!'
end
end
obj = SendTest.new
obj.send("testing")
obj.send(:testing)
OUTPUT
Hey there!
Hey there!
In your case, Consider the argument passed for colour is blue,
"has_#{colour}_colour_cell?" will return the string"has_blue_colour_cell?" and send will dynamically invoke the method named has_blue_colour_cell?. Same is the case for method blue_colour_cell
Direct answer to your question
You can dynamically get/set instance vars with:
instance_variable_get("#build_string_as_you_see_fit")
instance_variable_set("#build_string_as_you_see_fit", value_for_ivar)
But...
A Warning!
I think dynamically creating variables here and/or using things like string-building method names to send are a bad idea that will greatly hinder future maintainability.
Think of it this way: any time you see method names like this:
click_blue_button
click_red_button
click_green_button
it's the same thing as doing:
add_one_to(1) // instead of 1 + 1, i.e. 1.+(1)
add_two_to(1) // instead of 1 + 2, i.e. 1.+(2)
add_three_to(1) // instead of 1 + 3, i.e. i.+(3)
Instead of passing a meaningful argument into a method, you've ended up hard-coding values into the method name! Continue this and eventually your whole codebase will have to deal with "values" that have been hard-coded into the names of methods.
A Better Way
Here's what you should do instead:
class HomePage < SitePrism::Page
set_url '/index.html'
elements :color_cells, "div[id^='colour-cell-']"
def click_cell(color)
cell = color_cells.find_by(id: "colour-cell-#{color}") # just an example, I don't know how to do element queries in site-prism
cell.click
end
end
Or if you must have them as individual elements:
class HomePage < SitePrism::Page
set_url '/index.html'
COLORS = %i[red green blue]
COLORS.each do |color|
element :"#{color}_colour_cell", "div[id='colour-cell-#{color}']"
end
def cell(color:) # every other usage should call this method instead
#cells ||= COLORS.index_with do |color|
send("#{color}_colour_cell") # do the dynamic `send` in only ONE place
end
#cells.fetch(color)
end
end
home_page.cell(color: :red).click

Mutating list of instantiated objects in Rails

I'm attempting to clean up my view by moving Rails' sanitizer method to a helper, but it's not producing the desired result. So below is what my index action looks like. I know it's ugly and not very OOP, but I simplified it down so I could follow what was happening when debugging.
I'm attempting to loop through all the sources' attributes, running the sanitizer on any attribute that is a non-empty string, replacing original strings with the sanitized strings (transform_values!), and writing over the original #sources (map!).
I tried storing them in different variables than #sources and using .each instead of .map! but the sanitized values don't make it through.
def index
#sources = Source.all
#sources.map! { |source|
source.attributes.transform_values! { |attr|
attr.blank? || !attr.is_a?(String) ? attr
: ActionController::Base.helpers.sanitize(attr) } }
end
However, after examining my list of sources in the view, it's removing the source instances and instead returning a nondescript array of hashes. I can loop through these, but I can't call specific attributes like source.author which is not great.
Here's some images for reference. The first one is what it should look like and second is what I'm currently getting
Unsanitized sources
Sanitized sources
map! replaces each item in the array with the result of the block. This is not what you intend to do, because you just want to mutate the items, not replace them with something else. Use a plain each instead of map! would do the trick.
On another side, sanitization is actually a responsibility of the view (that’s why it’s defined in a helper). If you need to sanitize often with the same argument, define your own helper:
class ApplicationHelper
def sany(str)
sanitize(str, %w[...])
end
end
<%= sany(source.some_attr) %>
You could also set the default sanitization options following the documentation:
# In config/application.rb
config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
config.action_view.sanitized_allowed_attributes = ['href', 'title']

Why does the nil value or empty array returned from a capybara query have a text property?

Crazy question. I'm testing an angularjs app using capybara, webdriver, and firefox. I have a method that return the result of a scoped query:
def all_list_items
page.within(list_container) { page.all(list_item) }
end
And a second method to retrieve the text property of the first result.
def first_result_text
all_list_items.first.text
end
Sometimes this returns an empty array, ie. it didn't find anything.
When this happens I call .text on the empty array and it gives me all of the visible text on the screen.
When I call .text on the first item of the empty array (so calling it on nil) it return all of the visible text on the screen.
I'm very confused.
#all returns a Capybara::Result which doesn't have a #text method - so what class is the "empty array" you're seeing?

Return elements that match as an array

I am new to the Watir world, having used webdriver and geb in a previous company. I want to know if Watir offers any method that is analogous to the get_elements method from webdriver. See below for an example.
Imagine the following html exists within a larger page
<div class="someClass">someText</div>
<div class="someClass">someMoreText</div>
<div class="someClass">evenMoreText</div>
I want make some assertion against each of the divs by locating all elements of the given class and iterating through them. Using webdriver, I could do it like this:
elements = driver.get_elements(:css, ".someClass")
elements.each do |element|
//some assert on element
end
Does Watir provide an equivalent construct? I can't find anything useful in the Watir documentation.
You can do:
elements = driver.elements(:css => '.someClass')
Or if you know they are all divs, you should do:
elements = driver.divs(:css => '.someClass')
Both of these methods would return a collection of elements that match your criteria. In the first case it would match any tag type, where as the second case the results would be limited to divs.
Then, with either of the above, you can iterate the same way:
elements.each do |element|
//some assert on element
end
Instead of using :css locator i'd recommend you to use :class locator instead, since it is usually faster and makes your tests more readable:
elements = driver.divs(:class => 'someClass')
Also, don't forget :id, :name, :text and others.

Strange ruby syntax

what the syntax is in Action Mailer Basics rails guide ?
class UserMailer < ActionMailer::Base
def welcome_email(user)
recipients user.email
from "My Awesome Site Notifications <notifications#example.com>"
subject "Welcome to My Awesome Site"
sent_on Time.now
body {:user => user, :url => "http://example.com/login"}
end
end
How should i understand the construction, like
from "Some text for this field"
Is it an assignment the value to a variable, called "from" ?
No, it's a method call. The name of the method is from, and the argument is a string. In Ruby, parentheses around method calls are optional, so
from "Some text for this field"
is the same as
from("Some text for this field")
Rails (and many Ruby libraries) like to express code in a natural language style, though, so the parentheses-less version reads better, hence why it is used in examples.
It is a call to a method from with the argument "Some text for this field"
The method comes from the ActionMailer::Base class that your UserMailer extends from.
In Ruby the parentheses around a method call are optional unless something would be ambiguous so the statement is equivalent to from("Some text for this field")
Rails has a coding style that prefers to be close to natural language where possible, hence not using parentheses unless necessary.
Calling this method sets an instance variable #from to the value you provide so that it can be used later when sending the message.
Normally when you have accessor methods for getting and setting a variable you would have from= to set the value and from to return the value, however ActionMailer uses something called adv_attr_accessor to define the from method so that if you call it with a parameter then it acts as a setter but if you call it with no parameters then it acts as a getter.
This can be seen in actionmailer-2.x.x/lib/action_mailer/base.rb and actionmailer-2.x.x/lib/action_mailer/adv_attr_accessor.rb
It's not an assignment. In Ruby, assignments are done using the assignment operator = like this:
var = val
You are probably thinking of some Lisp dialects where assignment looks like this:
(def var val)
It's just a simple receiverless message send.
In Ruby, the general syntax for a message send is
receiver.selector(argument1, argument2)
However, if the receiver is self, you can leave off the receiver, so
selector(argument1, argument2)
is the same as
self.selector(argument1, argument2)
[Note: this is not quite true. In Ruby, private methods can only be invoked via a receiverless message send, so if in this example self responds to the selector message by invoking a private method, only the first variant will work, the second will raise a NoMethodError exception.]
Also, in cases where there are no ambiguities, you can leave off the parentheses around the arguments like this:
receiver.selector argument1, argument2
If you put the two things together, you can now see that
selector argument1, argument2
is equivalent to
self.selector(argument1, argument2)
and thus
from "Some text for this field"
is equivalent to
self.from("Some text for this field")
There is a third shortcut in Ruby's message sending syntax: if the very last argument to a message send is a Hash literal, then you can leave out the curly braces. So, the last line in the above example could also be written as
body :user => user, :url => "http://example.com/login"
Also, in Ruby 1.9, a Hash literal where all keys are Symbols can be written using an alternative Hash literal syntax:
{ key1: val1, key2: val2 }
is the same as the old syntax
{ :key1 => val1, :key2 => val2 }
which means that, at least in Ruby 1.9, that last line could also be written as
body user: user, url: "http://example.com/login"
You could also call from an attribute. It's a property of the email, but how it's implemented is hidden from you (encapsulation). This is a Good Thing. It means that if Rails core decided it's better to change #from into several variables, you wouldn't need to change any of your code.

Resources