How to define custom locating strategy for select - ruby

I looking for a proper way to redefine/extend locating strategy for select tag in Gwt app.
From html snippet you can see that select tag is not visible.
So to select option from list I need to click on button tag, and than select needed li tag from dropdown.
<div class="form-group">
<select class="bootstrap-select form-control" style="display: none; locator='gender">
<div class="btn-group">
<button class="dropdown-toggle" type="button" title="Male">
<div class="dropdown-menu open">
<ul class="dropdown-menu inner selectpicker" role="menu">
<li data-original-index="1"> (contains a>span with option text)
.....more options
</ul>
</div>
</div>
</div>
I see dirty solution: to implement method in BasePage class. This approach nice page_object sugar(options,get value, etc):
def set_nationality(country, nationality='Nationality')
select = button_element(xpath: "//button[#title='#{nationality}']")
select.click
option = span_element(xpath: "//span[.='#{country}']")
option.when_visible
option.click
end
Is there any other more clear way to do so? Using `PageObject::Widgets maybe?
UPD: Here what I expect to get:
def bool_list(name, identifier={:index => 0}, &block)
define_method("#{name}_btn_element") do
platform.send('button_for', identifier.clone + "//button")
end
define_method("#{name}?") do
platform.send('button_for', identifier.clone + "//button").exists?
end
define_method(name) do
return platform.select_list_value_for identifier.clone + '/select' unless block_given?
self.send("#{name}_element").value
end
define_method("#{name}=") do |value|
return platform.select_list_value_set(identifier.clone + '/select', value) unless block_given?
self.send("#{name}_element").select(value)
end
define_method("#{name}_options") do
element = self.send("#{name}_element")
(element && element.options) ? element.options.collect(&:text) : []
end
end

The select list appears to have the most identify attributes, therefore I would use it as the base element of the widget. All of the other elements, ie the button and list items, would need to be located with respect to the select list. In this case, they all share the same div.form-group ancestor.
The widget could be defined as:
class BoolList < PageObject::Elements::SelectList
def select(value)
dropdown_toggle_element.click
option = span_element(xpath: "./..//span[.='#{value}']")
option.when_visible
option.click
end
def dropdown_toggle_element
button_element(xpath: './../div/button')
end
def self.accessor_methods(widget, name)
widget.send('define_method', "#{name}_btn_element") do
self.send("#{name}_element").dropdown_toggle_element
end
widget.send('define_method', "#{name}?") do
self.send("#{name}_btn_element").exists?
end
widget.send('define_method', name) do
self.send("#{name}_element").value
end
widget.send('define_method', "#{name}=") do |value|
self.send("#{name}_element").select(value)
end
widget.send('define_method', "#{name}_options") do
# Since the element is not displayed, we need to check the inner HTML
element = self.send("#{name}_element")
(element && element.options) ? element.options.map { |o| o.element.inner_html } : []
end
end
end
PageObject.register_widget :bool_list, BoolList, :select
Notice that all locators are in relation to the select list. As well, notice that we use the accessor_methods to add the extra methods to the page object.
The page object would then use the bool_list accessor method. Note that the identifier is for locating the select element, which we said would be the base element of the widget.
class MyPage
include PageObject
bool_list(:gender, title: 'Gender')
bool_list(:nationality, title: 'Nationality')
end
The page will now be able to call the following methods:
page.gender_btn_element.click
page.gender_btn_element.exists?
page.gender
page.gender = 'Female'
page.gender_options
page.nationality_btn_element.click
page.nationality_btn_element.exists?
page.nationality
page.nationality = 'Barbados'
page.nationality_options

Related

Use a here string and lambda in a Sinatra helper method

I'm trying to create a Sinatra helper that returns a dynamically-generated HTML. I thought I would use a here string for the static bit and a lambda to calculate the dynamic part.
foo_helper.rb:
require 'erb'
module FooHelper
def tabs(selected)
template = ERB.new <<~HERE
<ul class="nav nav-tabs">
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'favorites' %>" href="/foo/favorites">Favorites</a></li>
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'all' %>" href="/foo">All</a></li>
<%= alpha.call %>
</ul>
HERE
# binding to a string works as expected
# alpha = "<li class='nav-item'><a class='nav-link' href='/foo/a'>A</a></li>"
# binding to a lambda, doesn't
alpha = lambda {
('a'..'z').each do |letter|
"<li class='nav-item'><a class='nav-link #{ 'active' if selected == letter }' href='/foo/#{letter}'>#{letter}</a></li>"
end
}
template.result(binding)
end
end
foo_controller.rb:
class FooController < ApplicationController
helpers FooHelper if defined?(FooHelper)
...
end
index.erb:
...
<%= tabs('favorites') %>
...
Results:
Displays the range, rather than the individual lis.
Am I missing something in the lambda?
** edit **
Corrected the numerous errors.
There are just too many mistakes in your code.
#nav is defined at module level, but accessed at instance level, so you got nil when you need it.
When you call a lambda, you need a dot between the variable name and the opening parenthesis, like foo.(123)
#nav.foo(binding), really? What's the (supposed) type of #nav? Does that type have the instance method foo?
<%= foo %> won't execute foo, because it's a local variable, not a method.
The reason why it renders a..z is because the Range#each method (called from within the lambda) executes a given block for each element and then returns the range itself again.
What you would want to use here instead is the Enumerable#map method. Similar to #each it also executes a block for each element, but the return values of said block are returned in a new array.
For comparison:
p ("a".."c").each { |x| x.upcase }
#=> "a".."c"
p ("a".."c").map { |x| x.upcase }
#=> ["A", "B", "C"]

Watir::ElementCollection click action in loop

Going off the example from the documentation http://www.rubydoc.info/gems/watir-webdriver/0.6.11/Watir/ElementCollection#each-instance_method, I am trying to click each element on the page that has the same class.
This is a code snippet of what I've come up with so far:
#b.divs(:class => 'portal-thumbnail-card').each do |div|
#b.div(:class => 'portal-thumbnail-card').click
puts 'foo'
# my puts statement outputs 'foo' 6 times (matches the number of elements with that class)
# right now this only clicks on the FIRST element, having issues with the other part :(
end
Even though this doesn't involve any page reloading, are click actions possible?
The problem is that you are locating the div to click during each iteration of the loop. In English, your code actually says, "for each div element with the class 'portal-thumbnail-card', click the first div on the page with class 'portal-thumbnail-card'."
What you actually want to do is click the div element that is the subject of each iteration:
#b.divs(:class => 'portal-thumbnail-card').each do |div|
div.click
puts 'foo'
end
The divs method returns a Watir::DivCollection, which is a collection of Watir::Div objects. For example:
require 'watir-webdriver'
b = Watir::Browser.new
b.goto('http://example.org')
divs = b.divs
puts divs.class
#=> Watir::DivCollection
divs.each { |d| puts d.class}
#=> Watir::Div
So--within your iterator--you want to refer to the block-local variable (i.e. div.click) instead of the browser's instance variable (i.e. #b.div(:class => 'portal-thumbnail-card').click)
use flash method for see element what you try click
require 'watir-webdriver'
browser = Watir::Browser.new
browser.goto "data:text/html,#{DATA.read}"
browser.divs(:class => 'portal-thumbnail-card').each do |div|
# browser.div(:class => 'portal-thumbnail-card').flash #you variant
div.flash #correct variant
puts 'foo'
end
browser.close
__END__
<html>
<div class='portal-thumbnail-card'>
<button id="button1">Button 1</button>
</div>
<div class='portal-thumbnail-card'>
<button id="button2">Button 2</button>
</div>
<div class='portal-thumbnail-card'>
<button id="button3">Button 3</button>
</div>
<div class='portal-thumbnail-card'>
<button id="button4">Button 4</button>
</div>
<div class='portal-thumbnail-card'>
<button id="button5">Button 5</button>
</div>
</html>

looping through a collection of divs in Watir

We're using watir for testing and wondered how to select a group of divs that meet a particular criteria? In our case the (simplified) html looks like this:
<div class="month_main>
<div class="month_cell">
some divs
</div>
<div class="month_cell">
some_other_divs
</div>
<div class = "month_cell OverridenDay">
<div id = "month-2013-05-04"/>
</div>
</div>
We would like to loop through all divs with an id starting with 'month' that are contained in month_cell parent divs that also have classes of OverridenDay. Is there an Xpath or regular expression we can use in conjunction with the Watir browser class to do this?
General
You can get a collection of elements in a similar way to getting a single element. You basically need to pluralize the element type method. For example:
#Singular method returns first matching div
browser.div
#Pluralized method returns all matching divs
browser.divs
Collections can be used using the same locators as single elements.
Solution
For your problem, you can do:
#Iterate over divs that have the class 'month_cell OverridenDay'
browser.divs(:class => 'month_cell OverridenDay').each do |overridden_div|
#Within each div with class 'month_cell OverridenDay',
# iterate over any children divs where the id starts with month
overridden_div.divs(:id => /^month/).each do |div|
#Do something with the div that has id starting with month
puts div.id
end
end
#=> "month-2013-05-0"
If you need to create a single collection that includes all of the matching divs, you will need to use a css or xpath selector.
Using a css-selector (note that in watir-webdriver, only the elements method supports css-locators):
divs = browser.elements(:css => 'div.month_cell.OverridenDay div[id^=month]')
divs.each do |e|
puts e.id
end
#=> "month-2013-05-0"
Using xpath:
divs = browser.divs(:xpath => '//div[#class="month_cell OverridenDay"]//div[starts-with(#id, "month")]')
divs.each do |e|
puts e.id
end
#=> "month-2013-05-0"

capybara - Find with xPath is leaving the within scope

I am trying to build a date selector with Capybara using the default Rails date, time, and datetime fields. I am using the within method to find the select boxes for the field but when I use xPath to find the correct box it leaves the within scope and find the first occurrence on the page of the element.
Here is the code I am using. The page I am testing on has 2 datetime fields but I can only get it to change the first because of this error. At the moment I have an div container with id that wraps up the datetime field but I do plan on switching the code to find by the label.
module Marketron
module DateTime
def select_date(field, options = {})
date_parse = Date.parse(options[:with])
year = date_parse.year.to_s
month = date_parse.strftime('%B')
day = date_parse.day.to_s
within("div##{field}") do
find(:xpath, "//select[contains(#id, \"_#{FIELDS[:year]}\")]").select(year)
find(:xpath, "//select[contains(#id, \"_#{FIELDS[:month]}\")]").select(month)
find(:xpath, "//select[contains(#id, \"_#{FIELDS[:day]}\")]").select(day)
end
end
def select_time(field, options = {})
require "time"
time_parse = Time.parse(options[:with])
hour = time_parse.hour.to_s.rjust(2, '0')
minute = time_parse.min.to_s.rjust(2, '0')
within("div##{field}") do
find(:xpath, "//select[contains(#id, \"_#{FIELDS[:hour]}\")]").find(:xpath, "option[contains(#value, '#{hour}')]").select_option
find(:xpath, "//select[contains(#id, \"_#{FIELDS[:minute]}\")]").find(:xpath, "option[contains(#value, '#{minute}')]").select_option
end
end
def select_datetime(field, options = {})
select_date(field, options)
select_time(field, options)
end
private
FIELDS = {year: "1i", month: "2i", day: "3i", hour: "4i", minute: "5i"}
end
end
World(Marketron::DateTime)
You should specify in the xpath that you want to start with the current node by adding a . to the start:
find(:xpath, ".//select[contains(#id, \"_#{FIELDS[:year]}\")]")
Example:
I tested an HTML page of this (hopefully not over simplifying your page):
<html>
<div id='div1'>
<span class='container'>
<span id='field_01'>field 1</span>
</span>
</div>
<div id='div2'>
<span class='container'>
<span id='field_02'>field 2</span>
</span>
</div>
</html>
Using the within methods, you can see your problem when you do this:
within("div#div1"){ puts find(:xpath, "//span[contains(#id, \"field\")]").text }
#=> field 1
within("div#div2"){ puts find(:xpath, "//span[contains(#id, \"field\")]").text }
#=> field 1
But you can see that but specifying the xpath to look within the current node (ie using .), you get the results you want:
within("div#div1"){ puts find(:xpath, ".//span[contains(#id, \"field\")]").text }
#=> field 1
within("div#div2"){ puts find(:xpath, ".//span[contains(#id, \"field\")]").text }
#=> field 2

How to build nested menu "trees" in HAML

I am trying to build a simple nested html menu using HAML and am not sure how to go about inserting the elements with the correct indentation, or the general best way to build nested trees. I would like to be able to do something like this, but infinitely deep:
- categories.each_key do |category|
%li.cat-item{:id => "category-#{category}"}
%a{:href => "/category/#{category}", :title => "#{category.titleize}"}
= category.titleize
It feels like I should be able to accomplish this pretty easily without resorting to writing the tags by hand in html, but I'm not the best with recursion. Here is the code I've currently come up with:
View Helper
def menu_tag_builder(array, &block)
return "" if array.nil?
result = "<ul>\n"
array.each do |node|
result += "<li"
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
attributes.each { |k,v| result += " #{k.to_s}='#{v.to_s}'"}
result += ">\n"
result += text
result += menu_tag_builder(node["children"], &block)
result += "</li>\n"
end
result += "</ul>"
result
end
def menu_tag(array, &block)
haml_concat(menu_tag_builder(array, &block))
end
View
# index.haml, where config(:menu) converts the yaml below
# to an array of objects, where object[:children] is a nested array
- menu_tag(config(:menu)) do |attributes, node|
- attributes[:class] = "one two"
- node["title"]
Sample YAML defining Menu
menu:
-
title: "Home"
path: "/home"
-
title: "About Us"
path: "/about"
children:
-
title: "Our Story"
path: "/about/our-story"
Any ideas how to do that so the output is like this:
<ul>
<li class='one two'>
Home
</li>
<li class='one two'>
About Us
</li>
</ul>
...not like this:
<ul>
<li class='one two'>
Home</li>
<li class='one two'>
About Us</li>
</ul>
... and so it's properly indented globally.
Thanks for the help,
Lance
The trick to nicely-indented, Ruby-generated Haml code is the haml_tag helper. Here's how I'd convert your menu_tag method to using haml_tag:
def menu_tag(array, &block)
return unless array
haml_tag :ul do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
haml_tag :li, text, attributes
menu_tag_builder(node["children"], &block)
end
end
end
How about something along the lines of:
def nested_list(list)
return unless list
haml_tag :ul do
list.each do |item|
haml_tag :li do
haml_concat link_to item["title"], item["path"]
if item["children"]
nested_list item["children"]
end
end
end
end
end
Awesome, #shingara's hint put me in the right direction :). This works perfectly:
def menu_tag(array, &block)
return "" if array.nil?
haml_tag :ui do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node[:title]
end
haml_tag :li, attributes do
haml_concat text
menu_tag_builder(node[:children], &block)
end
end
end
end
If somebody can make that even shorter, or make it more easy to customize the attributes on the nested nodes, I'll mark that as correct instead of this.
Cheers.
It's because you send a pur HTML by your helper. The indentation become with HAML. You can can generate some HAML in your helper.

Resources