is 'element.wait_until' broken in page_object 2.0.0? - watir

Using Watir 6.0.3, page object 2.0.0 and Ruby 2.1.9
As per latest watir and page-object changes changed below code to
wait_until(DEFAULT_WAIT_TIME.to_i, 'Login button not found when waiting for the login page to load') do
login_element.visible?
end
to
message = "Login button not found when waiting for the login page to load"
login_element.wait_until(timeout: timeout, message: message, &:visible?)
but getting undefined method 'zero?' for #<Hash:0x4991340> (NoMethodError)error.
however if I get rid of page-object locator shown below Watir 'wait_until'works as expected.
message = "Login button not found when waiting for the login page to load"
browser.button(name: 'login').wait_until(timeout: 10, message: message, &:visible?)

The Element#wait_until method is defined as:
def wait_until(timeout=::PageObject.default_element_wait, message=nil, &block)
Object::Watir::Wait.until(timeout: timeout, message: message, &block)
end
Notice that timeout and message are normal parameters rather than keyword arguments. As a result, the usage needs to be:
login_element.wait_until(timeout, message, &:visible?)
That said, Element#wait_until is still broken. The way that Object::Watir::Wait.until is called will result in a NoMethodError due to object being nil in the Watir method. Until a fix is released, you can monkey patch Page-Object using (included after you require 'page-object'):
module PageObject
module Platforms
module WatirWebDriver
module Element
def wait_until(timeout=::PageObject.default_element_wait, message=nil, &block)
element.wait_until(timeout: timeout, message: message, &block)
end
end
end
end
end

Related

Selenide test not able to load correct element on a newly opened tab/window

I am using Selenide to open a sample input form on w3schools.com (found here).
The test inputs a name into the form and clicks submit, which then opens a new page in a separate tab/window which displays the request params in a div.
The following is my test code:
import com.codeborne.selenide.Configuration
import org.openqa.grid.internal.utils.configuration.StandaloneConfiguration
import org.openqa.selenium.remote.server.SeleniumServer
import spock.lang.Specification
import static com.codeborne.selenide.Condition.*
import static com.codeborne.selenide.Selectors.byName
import static com.codeborne.selenide.Selectors.byTitle
import static com.codeborne.selenide.Selenide.*
class DummyTest extends Specification {
def "Test w3schools form submit"() {
given: "Server setup configurations"
Configuration.browser = 'chrome'
StandaloneConfiguration configuration = new StandaloneConfiguration()
SeleniumServer server = new SeleniumServer(configuration)
server.boot()
when: "Name info is submitted into the form"
open("http://www.w3schools.com/html/html_forms.asp")
$(byName("firstname")).setValue("Clark")
$(byName("lastname")).setValue("Kent")
$x('//*[#id="main"]/div[3]/div/form/input[3]').click()
// Maybe this will fix the erroneous element problem?
$(byTitle('HTML Forms')).waitUntil(disappears, 5000)
then: "New page should display the passed-in request params"
$x("/html/body/div[1]").shouldHave(text("firstname=Clark&lastname=Kent"))
}
}
...where webdriver.chrome.driver is a passed-in environment variable pointing to the correct location of chromedriver.exe.
The test does open the initial form page and type in the first/last name correctly. When it clicks the submit button, it waits for the second page to load and display the request params.
As a note, the error happens regardless of whether or not my waitUntil() method call is present or not. Many of the other StackOverflow posts suggest doing something like that to fix the problem, though the Selenide docs say that the shouldHave() method call in my then clause should automatically do a fluent wait.
This is the output of my test case:
Condition failed with Exception:
$x("/html/body/div[1]").shouldHave(text("firstname=Clark&lastname=Kent"))
| | |
| | text 'firstname=Clark&lastname=Kent'
| Element should have text 'firstname=Clark&lastname=Kent' {By.xpath: /html/body/div[1]}
| Element: '<div class="w3-container top">w3schools.com
| THE WORLD'S LARGEST WEB DEVELOPER SITE</div>'
| Screenshot: file:/C:/Workspace/IntelliJ/dummy-selenide-project/build/reports/tests/1498836183270.0.png
| Timeout: 4 s.
NoSuchElementException: no such element: Unable to locate element: {"method":"xpath","selector":"/html/body/div[1]"}
So, it's clear that, as per the test output, it's grabbing the xPath of the initial page, not the second page.
Is there something that I'm missing or doing wrong? Do I need to instantiate a new object or something for every new page?
It was difficult to find the answer in the docs, but I found out in a question here that you do need to call a method to switch to another window.
You must call the static method Selenide#switchTo(), which returns a SelenideTargetLocator. On that object, you can call the method window(), which either takes a 0-based index (int) or a name/handle/title (String).
So, after adding the switchTo() method in my and clause for my Spock test, the working result looks like this:
def "Test w3schools form submit"() {
given: "Server setup configurations"
Configuration.browser = 'chrome'
StandaloneConfiguration configuration = new StandaloneConfiguration()
SeleniumServer server = new SeleniumServer(configuration)
server.boot()
when: "Name info is submitted into the form"
open("http://www.w3schools.com/html/html_forms.asp")
$(byName("firstname")).setValue("Clark")
$(byName("lastname")).setValue("Kent")
$x('//*[#id="main"]/div[3]/div/form/input[3]').click()
and: "Switch to newly opened window"
switchTo().window(1)
then: "New page should display the passed-in request params"
$x("/html/body/div[1]").shouldHave(text("firstname=Clark&lastname=Kent"))
}
Note: For sake of my test code, since the test only opens one new tab, calling switchTo(1) works fine here for Chrome, but note that the window/tab order may be different in other browsers.

Is `#browser` the obligatory name for the ChromeDriver object when using Watir/Chrome?

I want to change the name of the #browser variable because I'm tired of typing it.
In env.rb:
before do
#b = Watir::Browser.new :chrome
end
This throws error:
Unable to pick a platform for the provided browser or element: nil.
nil was passed to the PageObject constructor instead of a valid browser or element object. (RuntimeError)
before do
#browser = Watir::Browser.new :chrome
end
works as expected.
#browser = Watir::Browser.new :chrome
#b = #browser
doesn't work either. Is there any way to modify this variable name?
In general, you can store your browser instance in a variable of any name. Watir-Webdriver does not care, nor does Cheezy's page objects. The initialization of the page object only cares what the actual object passed in is - ie it must be a Watir::Browser or Watir::Element (or the Selenium-WebDriver equivalents).
However, this is not true when using the PageObject::PageFactory. In the on_page method, and implicitly visit_page and if_page, the code is hard-coded to look for a #browser variable.
def on_page(page_class, params={:using_params => {}}, visit=false, &block)
page_class = class_from_string(page_class) if page_class.is_a? String
return super(page_class, params, visit, &block) unless page_class.ancestors.include? PageObject
merged = page_class.params.merge(params[:using_params])
page_class.instance_variable_set("#merged_params", merged) unless merged.empty?
#current_page = page_class.new(#browser, visit)
block.call #current_page if block
#current_page
end
The initialization of the page object is always done with #browser. When you stored the browser instance in #b, it meant that #browser was nil. This then led to the exception you saw. If you want to use the PageFactory as written, you will need to stick to #browser.
If the variable really bothers you, there is always the option for monkey patching. Simply re-define the on_page and on method to use whatever variable you like. The following redefines the method to use #b:
require 'page-object'
module PageObject
module PageFactory
def on_page(page_class, params={:using_params => {}}, visit=false, &block)
page_class = class_from_string(page_class) if page_class.is_a? String
return super(page_class, params, visit, &block) unless page_class.ancestors.include? PageObject
merged = page_class.params.merge(params[:using_params])
page_class.instance_variable_set("#merged_params", merged) unless merged.empty?
#current_page = page_class.new(#b, visit)
block.call #current_page if block
#current_page
end
alias_method :on, :on_page
end
end
The issue is somewhere else in your code. If you run just the line you provided (after requiring watir)
require 'watir-webdriver'
#b = Watir::Browser.new :chrome
in a IRB session it will work just fine..
Some other code dependency (page objects library perhaps) needs a browser object and is expecting a specific name, is getting a nill (un-initialized) object instead and complaining.
Also, unless you want the overhead of opening and closing the browser for each test (can add a ton of time) you may want to initialize the browser earlier, such as in a before_all hook, or early in the startup of your test code. Then just do something like clear cache and cookies in the before hook

Using previous_changes in after_save callback with rails_admin

I have an after_save callback that uses the previous_changes method to test for field change. It works fine when I save a record in rails console, but fails to recognise that the field has changed when I try to trigger it in rails_admin.
def mailers
if previous_changes['pending_approval'] == [false, true]
ProjectMailer.project_owner_new_project(self).deliver_later
Rails.logger.debug("Changed Pending to False!!".green)
end
Rails.logger.debug("hit project mailer callback".red)
end
In this example, when run in console (with a record that has just changed pending_approval from true to false) I get two logged messages; "changed pending to False!!" and "hit project mailer callback"
When I change the state in rails_admin, I only get the second "hit project mailer callback." This leads me to believe that rails_admin is doing something that interferes with the Dirty class handling of my record. Is there a better way to do this?
One way to do this is to change the after callback to before and use self_attribute_changed?(from: true, to: false) like this:
def mailers
if self.pending_approval_changed?(from: true, to: false)
ProjectMailer.project_owner_new_project(self).deliver_later
Rails.logger.debug("Changed Pending to False!!".green)
end
Rails.logger.debug("hit project mailer callback".red)
end
I suppose another way that would utilize the same strategy would be to use the around_save like so
def mailers
changed = self.pending_approval.changed?(from: true, to: false)
yield
if changed
ProjectMailer.project_owner_new_project(self).deliver_later
Rails.logger.debug("Changed Pending to False!!".green)
end
Rails.logger.debug("hit project mailer callback".red)
end
I tried the first way and it worked well for my situation. Haven't tested the second way.

Method call on page object does not return result

I am having problem to use page object method call to return an object on the page.
Here is my example codes in test_log_in.rb
...
class TestLogIn < Test::Unit::TestCase
.....
def test_failed_log_in
#log_in_page= LogIn.new(#browser)
#log_in_page.go_to_log_in
#log_in_page.log_in("174773476","test","aaa111as")
puts #log_in_page.error_message
puts #log_in_page.get_error_message
end
end
My log in class is defined below:
class LogIn
include PageObject
...
h3(:error_message, :class => 'no-margin white-text')
...
def log_in (access_number, user_id, password)
self.access_number = access_number
self.user_id = user_id
self.password = password
log_me_in
AccountSummary.new(#browser)
end
....
def get_error_message
self.error_message
end
....
end
Why will the following lines returns no output?
puts #log_in_page.error_message
puts #log_in_page.get_error_message
Can you please help me?
Thanks.
My guess is that there is a timing issue due to the error message being displayed through javascript. It is likely that the error message element is always in the html of the page, but when login fails, the error message element's style is changed to be something visible.
When a page object (or watir) element returns its text, it only includes the visible text. So if the error message is not yet visible, you get no text.
Try waiting to ensure the element is visible to the user:
def get_error_message
error_message_element.when_present.text
end
when_present will wait up to 5 seconds for the element to become visible to the user and then return the text.
Update - Multiple h3s:
It looks like the actual problem is that there are multiple h3 elements that have that class. Watir always returns the first match, which in this case is the cookie error heading. Given that it is not being displayed, you get the blank text.
The solution would be to reduce the search scope so that you the first h3 is the one you want. This can be done by only looking in the error message list. I would define the page object as follows. Note that you do not need to specify the class of the h3 element since it is the only h3 element in the list.
class LogIn
include PageObject
div(:error_message_list, :id => 'errorMessageList')
h3(:error_message){ error_message_list_element.h3_element }
end
This will then find the right h3 element:
browser = Watir::Browser.new
browser.goto 'https://www.bnz.co.nz/ib4b/app/login'
browser.button(:text => 'Login').click
page = LogIn.new(browser)
page.error_message
#=> "There were 3 errors on the page."

ActionDispatch::ClosedError when testing Rails 3.1 model creation (RSpec/Cucumber)

I am creating a web application with Ruby on Rails 3.1 (RC1). I am using Factory Girl, RSpec and Cucumber (with Capybara) for testing, but I am experiencing unexpected raised ActionDispatch::ClosedErrors some of the times (not every time) when I am creating new users (through the User model's create action). Below is the error message that I get:
Cannot modify cookies because it was closed. This means it was already streamed
back to the client or converted to HTTP headers. (ActionDispatch::ClosedError)
The error is raised when using these ways of creating users:
Creation using Factory Girl
Factory.create( :user )
Factory.build( :user ).save
Basic creation
User.create( { ... } )
User.new( { ... } ).save
What is funny is that they do work during some test, but not in others, and it does not seem random, although I cannot figure out the reason. Below is an excerpt from my code:
users_controller_spec.rb
require 'spec_helper'
def user
#user ||= Factory.create( :user )
end
def valid_attributes
Factory.attributes_for :user
end
describe UsersController do
describe 'GET index' do
it 'assigns all users as #users' do
users = [ user ] # The call to user() raises the error here
get :index
assigns[ :users ].should == users
end
end
describe 'GET show' do
it 'assigns the requested user as #user' do
get :show, id: user.id # The call to user() raises the error here
assigns[ :user ].should == user
end
end
However, the error is not raised in the following code block:
describe 'GET edit' do
it 'assigns the requested user as #user' do
get :edit, id: user.id # This raises no error
assigns[ :user ].should == user
end
end
Any other method below this does not raise the error, even though I am creating users in the exact same way.
Any suggestions to what I might be doing wrong would be greatly appreciated!
Someone posted a workaround here
https://github.com/binarylogic/authlogic/issues/262#issuecomment-1804988
This is due to the way rails 3 streams the response now. They posted a fix in edge for the same issue in flash but not in cookies yet. For now I have turned off my request specs. I am going to look at the problem this weekend if no one gets to it before then.
https://github.com/rails/rails/issues/1452
Just so we don't have to follow links, here's my modified version of the authlogic workaround:
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.maintain_sessions = false if Rails.env == "test"
end
end
Rather than deal with ensuring session management on every .save call, I just turn them off if I'm testing.

Resources