How do I assert within the first element of many in Capybara? - capybara

I want to do something like this:
within '.Collection .Thing:first-child' do
page.must_have_content 'lorem ipsum'
end
Where .Thing is just one of many other Things inside Collection. I tried the code above but it says it's ambiguous. Is there a way to resolve the ambiguity?

If you really just want the first matching element on the page you can do
within first('.Collection .Thing:first-child', minimum: 1) do
page.must_have_content 'lorem ipsum'
end
the minimum: 1 option isn't necessarily needed if the page is already fully loaded, however it will make Capybara wait for at least one matching element to show on the page if the page isn't yet fully loaded (or the list is filled via ajax, etc). Without it #first would normally return immediately with nil if no matching elements were on the page

Related

Watir - How do I collect all links where the span contains aria_label "Multimedia"

I have written a ruby code where the browser object finds all links and then I store them one by one in an array if they match a specific regex.
#browser.links.collect(&:href).each do |link|
matches = regex.match(link)
array_of_multimedia << matches[:multimedia_id] if matches
end
I am trying to create a filter where I only iterate over those links where the span inside the second child div contains the aria-label as Multimedia.
Attached is the screenshot of the HTML structure.HTML structure
I tried a few approaches like finding all spans and then going bottom up to the parent's parent of the span but its not giving me the href.
#browser.spans(aria_label: "Multimedia").each do |span|
span.parent.parent.a.hreflang #Didn't work
span.parent.parent.a.link.href #Didn't work
span.parent.parent.href.text #Didn't work
element.tag_name #This shows "a" which is correct though
end
I also tried a top down approach by doing
#browser.links.collect(&:href).each do |link|
link_element = #browser.link(href: link)
link_element.children.following_sibling(aria_label: "Multimedia").present? #Didn't work
end
So far, no luck in getting the actual hrefs. Will appreciate any help!
Because the span is inside the link tag, it's going to be easier to go bottom up
Do as much as you can with the Watir locators rather than multiple loops.
The parent method takes arguments:
#browser.spans(aria_label: 'Multimedia').map {|span| span.parent(tag_name: 'a').href }
As for what you tried:
# parent.parent is the link, so calling `#a` is looking for a link nested inside the link
span.parent.parent.a.hreflang
span.parent.parent.a.link.href
# href should give you a String, you shouldn't need to call #text method on it
span.parent.parent.href.text
# element isn't defined here, but try just element.href
element.tag_name
Also note that Element#href method is essentially a wrapper for Element#attribute_value('href').

Why is capybara saying node is obsolete and how to solve?

I have an spec/capybara test which searches for an element and then attempts to run a JS script to scroll the element into view. However, Capybara claims the node is obsolete by the time it attempts to run the JS.
The lines at issue are consecutive. Here they are:
element = page.find(selector, visible: false)
Capybara.current_session.driver.browser.execute_script(script, element.native)
I have done a fair bit of debugging already. When placing a debugger between the find and execute_script lines, calling element indeed returns an obsolete node Obsolete #<Capybara::Node::Element>.
Running page.find(selector, visible: false) within the debugger does not return an obsolete node but rather the normal active node you would expect #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[6]/DIV[2]/DIV[1]/DIV[1]/DIV[54]">
Furthermore, removing the two lines and running them manually in the debugger sees capybara correctly find the DOM element, run the JS correctly, and the spec passes
The relevant code:
def scroll_to(selector, align = true)
if align
script = <<-JS
arguments[0].scrollIntoView(true);
JS
else
script = <<-JS
arguments[0].scrollIntoView(false);
JS
end
element = page.find(selector, visible: false)
Capybara.current_session.driver.browser.execute_script(script, element.native)
end
scroll_to(".xdsoft_time[data-hour='13'][data-minute='15']")
Without knowing what's happening on your page it's impossible to say exactly why you're getting the 'obsolete node' error, but that error indicates the node that was originally found is no longer in the page. This can happen if you visit a new page, if the part of the page containing that node is replaced by JS, etc.
Passing visible: false and then trying to scroll that element into the page doesn't make sense though since if the element isn't visible then you'll never be able to scroll it into view (visible means drawn on the page, it does not mean 'in the viewport').
Other issues with your code are
you should not be calling the driver specific execute_script, but rather just use the Capybara session execute_script (generally if you're using Capybara.current_session.driver.browser you're doing something wrong).
page.execute_script(script, element)
Capybara already provides a scroll_to so use it instead of writing your own
page.scroll_to(page.find(selector)) # Defaults to scrolling to the top
If you need control over the alignment of the element just pass the :align option
page.scroll_to(page.find(selector), align: :center) # :top, :bottom, :center

Capybara Select2 Help w/Firefox

For some reason im running into problems with Select2 and Firefox w/Geckodriver.
Select2 fields I used to be able to just say page.select 'Text', from: 'Label' however that no longer works I just get a Element <option> could not be scrolled into view (Despite being scrolled into view). Right now im doing something similar to this:
select2Fields = page.all('.select2-selection')
select2Fields[0].click
page.find('.select2-search__field').set('Text To Set')
within('.select2-results') do
page.find('li', text: 'Text To Click').click
end
It's ugly and doesn't fit with my Page Object Model method, since I have to sorta know which select2 field it is. It doesn't seem to be when finding it with a label.
Any ideas? It's very frustrating since it worked with Chrome but the latest chromedriver has issues with the newest capybara versions.
Not sure what you were using that you were able to ever use select with a select2 widget, it never should have worked, and the fact it did would have been a bug. The reason is the actual <select> element (which is what Capybaras select method works with) is non-visible on the page, and select2 replaces it with a JS driven widget. You need to do exactly what a user would do, which is click to make the widget show up then click on the <li> element which represents the correct entry. This can all be moved into a helper method and potentially some custom selectors which boils down to something like this
Capybara.add_selector(:select2) do
xpath do |locator, **options|
xpath = XPath.descendant(:select)
xpath = locate_field(xpath, locator, options)
xpath = xpath.next_sibling(:span)[XPath.attr(:class).contains_word('select2')][XPath.attr(:class).contains_word('select2-container')]
xpath
end
end
Capybara.add_selector(:select2_option) do
xpath do |locator|
# Use anywhere to escape from the current scope since select2 appends
# the choices to the end of the document
xpath = XPath.anywhere(:ul)[XPath.attr(:class).contains_word('select2-results__options')][XPath.attr(:id)]
xpath = xpath.descendant(:li)[XPath.attr(:role) == 'treeitem']
xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
xpath
end
end
def select_from_select2(value, from: nil, **options)
select2 = if from
find(:select2, from, options.merge(visible: false))
else
select = find(:option, value, options).ancestor(:css, 'select', visible: false)
select.find(:xpath, XPath.next_sibling(:span)[XPath.attr(:class).contains_word('select2')][XPath.attr(:class).contains_word('select2-container')])
end
select2.click
find(:select2_option, value).click
end
That should let you call select_from_select2 just like you would call select and it will find the select2 widget associated with the given <select> element (hidden by select2) and choose the correct entry from it.
I had tested the Thomas´ answer but it doesn´t work for me. When Capybara click in the desired option, the select2 box close itself and set the 0 option. Finnaly, I made a walkaround as I check the option I want as selected and trigger the change.select2 event. I know that I dont really test the select2 box.
def self.select2 (page, datos)
page.execute_script("$('##{datos[:from]}').select2('open')")
if page.find(".select2-results li", text: datos[:texto]).click
page.execute_script("$('##{datos[:from]} option[value=\"#{datos[:valor]}\"]').prop('selected', true)")
page.execute_script("$('##{datos[:from]}').trigger('change.select2')")
end
page.find(:css, '#' + datos[:from]).value
end
As I keep my module Helper without include it in tests, I needed to include self in the name of the method and take 'page' from the capybara´test as parameter.
The variable 'datos' is a hash with the selector, the text of the option and its value.
As select2 box close when capybara click on it, I wrap the walkaround inside the if clause to be sure that some parts of the select2 box were working.
Finally, I returned the current value of the select to test it (really, it doesnt needed as I set the option with that value as 'selected')
I hope it would help anyone.

In rspec/capybara, I need a version of "within" that checks all descendants, not just children

I have a snippet of code designed to count the number of list items in a dropdown menu:
within ('#campaign_duration_in_days_input') do
page.all('li').count.should eql(4)
end
That returns 0, although there are four list items as descendants of that div (not direct children). How can I get a count of all descendants?
I can't see anything wrong with your code. within works with indirect descendants as well as direct children. It would also throw an error if the argument #campaign_duration_in_days_input so it looks like that element exists.
So looks like it is just not finding any li elements. If this is a plain old HTML select dropdown should you be looking for option elements? If not then try save_and_open_page immediately before the within to inspect the DOM to see what is happening.

Is it possible to interact with hidden elements with capybara?

I have a file field that has opacity: 0 and is overlaping a fake button. Its a common css technic to fake a sort of "Upload button" that displays consistently across different browsers.
Capybara doesn't allows me to call attach_file on that input. The error is Selenium::WebDriver::Error::ElementNotVisibleError: Element is not currently visible and so may not be interacted with.
Anybody knows any way to force capybara to interact with invisible elements?
The answer is still unanswered, but I've found a work around. Nothing intelligent, just make visible the element with a simple script
page.execute_script %Q{
$('#photos').css({opacity: 1, transform: 'none'});
}
I post it for the record.
You can interact with hidden elements using the visible: false property in Capybara.
If you want to click on hidden element use:
find("#photos", visible: false).click
Don't use click_button('#photo') directly
The author of Capybara recommends setting Capybara.ignore_hidden_elements immediately prior to needing to see the invisible element, and resetting it afterwards:
Capybara.ignore_hidden_elements = false
click_button 'my invisible button'
Capybara.ignore_hidden_elements = true
In general interacting with non-visible elements should not be possible when using Capybara (you can find them using the visible: false/hidden option in most finders but not actually do anything to them). However, the file input is a special case because of how common it is to hide the element and, due to security restrictions, no other way to actually add a file by interacting with the pages visible elements. Because of this attach_file has a make_visible option which can be used to have Capybara make the element visible, attach the file, and then reset the CSS to the original setting.
attach_file('photos', file_path, make_visible: true)
I ended up resolving it a different route.
execute_script() was giving me a hard time (it would freeze test execution on FireFox), so this is what I did:
I already had an appropriate javascript file. I appended the following
<% if ENV["RAILS_ENV"] == "test" %>
$('#photos').show()
<% end %>
I also had to append .erb to my javascript file for proper Rails asset handling.
And in my test file, I was already setting ENV["RAILS_ENV"] = "test"
This way I could just dumb down the UI for test, and yet maintain the look and feel for production.
Miquel, thanks for workaraund.
I have similar issue for interacting with hidden file input on C# binding for Selenium Webdriver 2.35 and Firefox 24. To make file selection working did similar trick:
((IJavaScriptExecutor)webdriver).ExecuteScript("$('#fileUploadInput').css({opacity: 1, transform: 'none'});");
IWebElement e = webdriver.FindElement(By.CssSelector("input#fileUploadInput")));
e.SendKeys("c:\\temp\\inputfile.txt");
I've done it this way with elements that has the CSS style display:none; set:
page.execute_script("$('.all-hidden-elements').show();");
all('.all-hidden-elements').first.click
If the hidden element is nested in a visible parent element (e.g. a hidden input inside a visible label), you can click on the parent instead. If you still want to find the input by ID, you can traverse to the parent like so:
find('#hidden_input').find(:xpath, '..').click

Resources