cheezy / page-object

Gem to implement PageObject pattern in watir-webdriver and selenium-webdriver
MIT License
653 stars 220 forks source link

Visit page with parameters #128

Closed knorrium closed 11 years ago

knorrium commented 12 years ago

Is it possible to visit a page with different parameters?

I tried adding a initialize method to the my page class, but failed while invoking the visit_page(MyPage) method.

I would like to do something like this:

class MyPage
  include PageObject

    def initialize(locale = nil)
        if (locale.nil?) then
            $url = ENV['HOST']
        else
            $url = ENV['HOST'] + "?locale=" + locale
        end
    end
  page_url $url

Perhaps a hash or an array of parameters to build the final url would be better.

Any ideas? Do you think this is a feature worth having?

Thanks,

FK

zeljkofilipin commented 12 years ago

This is how I do it:

def self.url
  "#{BASE_URL}dashboard/profile"
end
page_url url
knorrium commented 12 years ago

@zeljkofilipin the base url is okay and I'm using it with environment variables, I'm trying to pass parameters to the page class.

Some examples:

Thanks,

FK

zeljkofilipin commented 12 years ago

This is what I usually do:

homes_page.rb

class HomesPage
  include PageObject

  def self.url
    "#{BASE_URL}homes/"
  end
  page_url url

  def url_with_id(id)
    "#{self.class.url}#{id}"
  end
  def visit_page_with_id(id)
    navigate_to url_with_id(id)
  end
end

homes_steps.rb

Given /^I am at Homes page for home (\d+)$/ do |id|
  on(HomesPage).visit_page_with_id(id)
end
gshakhn commented 12 years ago

We have a similar problem and extended page object locally with the following:

module PageObject::Accessors

  def dynamic_url(url)
    define_method("generate_dynamic_url") do |id|
      url.sub("#id#", id.to_s)
    end
  end

  def with_id(id)
    define_method("goto") do
      platform.navigate_to self.generate_dynamic_url(id)
    end
    self
  end
end

Using it looks like:

class ThingPage
  include PageObject

  dynamic_url "#{your_host}/thing/#id#"
end

visit_page ThingPage.with_id(123) do |page|
  # stuff
end

The above just replaces the string '#id#' with the id you want. I wasn't sure how useful this would be to the larger community, so didn't open up a pull request. Could do so if it's desired though.

We're also considering a usage like:

dynamic_url "#{your_host}/thing?p1=#p1#&p2=#p2#"

visit_page ThingPage.with_params(:p1 => 'stuff1', :p2 => 'stuff2')

This would let you have as many params as you want with whatever names.

knorrium commented 12 years ago

@gshakhn your idea of the with_params method seems to be exactly what I had in mind but didn't know how to implement :) I would love to see this on master.

What do you think, @cheezy?

@zeljkofilipin thanks for your input as well :)

gshakhn commented 12 years ago

The downside with my approach is that we modify that page object's class permanently. So if you did:

visit_page ThingPage.with_params(:id => 123)

It would hit 'thing/123', which is what you want. However, if you then did:

visit_page ThingPage

You would still hit 'thing/123'. This feels bad, but I'm not sure how to get around it cleanly.

In theory, you could make the with_params method flip a switch that tells the goto method to use the dynamic url. Once it's used, then the switch flips off and subsequent invocations of visti_page with that page object will use the page_url url, unless that invocation has with_params. i.e.

def with_params
  @use_dynamic_url = true
  # save params somewhere
end

def goto
  if use_dynamic_url
    @use_dynamic_url = false
    goto_generated_dynamic_url
  else
    goto_static_page_url
end

I don't fully like the above solution though :(

Another thing that would be nice is if you could do:

visit_page ThingPage.with_id(123).with_locale('en-US')

instead of:

visit_page ThingPage.with_params(:id => 123, :locale => 'en-US')

But that would involve storing even more state.

gshakhn commented 12 years ago

There is also https://github.com/cheezy/page-object/issues/69, which has a different solution for this problem.

cheezy commented 12 years ago

Sorry guys. I've been doing some hard travel the past few days (3 days, 3 cities) and am just now catching up on this thread. There is a callback that happens during object creation but it happens after navigation (if you use visit_page). Here's the constructor from PageObject module.

  def initialize(browser, visit=false)
    initialize_browser(browser)
    goto if visit && respond_to?(:goto)
    initialize_page if respond_to?(:initialize_page)
  end

If you implement the initialize_page method it might be a way to provide some dynamic data to the page but it will clearly not work in the case of a dynamic url. For things that change from environment to environment (like base url) I use one of my other gems fig_newton.

I think it would not be nice to add the ability to pass parameters via the visit_page method. I could see it working something like this:

  visit_page(MyPage, :using_params => { :id => 'the_id', :locale => 'es' })

and in your page you would have access to these params using something like this:

  page_url "#{FigNewton.base_url}/account/#{params[:id]}"

I'll need to do some investigation to ensure it can be done without breaking the current functionality.

What do you guys think?

gshakhn commented 12 years ago

Sounds reasonable. It'll cover our use case and doesn't have my ugly hack. :)

bbbco commented 12 years ago

I think this would be very beneficial!

zeljkofilipin commented 12 years ago

I would like to see it implemented.

knorrium commented 12 years ago

+1 :D

cheezy commented 12 years ago

This is going to be a little more difficult than I originally thought. The page_url method is invoked when the class is evaluated (when it is read) and at that time the params are not available. They only become available when the visit_page method is called. I'll keep working on it to see what I come up with.

cheezy commented 12 years ago

The challenge I am having is that the string passed into page_url is evaluated at the same time the page object class is evaluated. This is long before any calls to visit_page. As a result, I am unable to use the parameters in my call to page_url. A simple solution might be to add a url parameter to visit_page which would override the url passed to page_url. Here's an example:

class MyPage
  include PageObject

  page_url "#{FigNewton.base_url}/page"
end

In a step definition the default behavior would be this:

visit_page(MyPage)

This would use the page defined the class. In order to provide a dynamic url you could do this:

visit_page(MyPage, :using_url => "#{FigNewton.base_url}/page/#{something_else}")

This would use the provided url instead of the default value. Would this work for your use cases?

zeljkofilipin commented 12 years ago

Works for me.

gshakhn commented 12 years ago

The drawback with passing in the full url is DRY. Our urls are long and unwieldy, with only one part different from test to test. It also seems weird that the test would know the url of the page instead of the page object figuring it out.

Instead of evaluating the url from page_url using string interpolation, could you use simple string substitution? Something like: (haven't actually tried this, may have syntax errors)

class MyPage
  include PageObject

  page_url "#{FigNewton.base_url}/page/#param1#"
end

Visit page usage would be like:

visit_page(MyPage, :using_params => {:param1 => 'stuff'} )

The definition of page_url could look something like:

def page_url(url)
  define_method(":goto") do |params|
    real_url = params.reduce(url) do |url, pair|
      url.sub("##{pair[0]}#", pair[1].to_s)
    end
    platform_navigate_to real_url
  end
end
cheezy commented 12 years ago

That could work or it could support something more standard like erb. In that case the url would be:

page_url "#{FigNewton.base_url}/page/<%=params[:encoding]%>"

You could then use the params similar to what you have above.

visit_page(MyPage, :using_params => {:encoding => 'en'})

This is something I should be able to get in and release this weekend.

gshakhn commented 12 years ago

Something standard would be better. :)

cheezy commented 12 years ago

Unless I hear from anybody else I think that is what I'll go with.

cheezy commented 12 years ago

I've just checked in code that does the following:

First of all there is a new instance variable @params that exists on your page object class. You can setup params that can be used in your url's like this:

class MyPage
  include PageObject

  params = { :encoding => 'en', :id => 3 }
  page_url "#{FigNewton.base_url}/somecontext/?id=<%=params[:id]%>?encoding<%=params[:encoding]"
end

Notice here that I am using both interpolation and erb. The interpolation happens first and then the erb is run with the params available. You can think of the params as the default values that are used. There is also an update to visit_page. With this update you can provide additional params that are merged with the defaults. Here's an example:

visit_page(MyPage, :using_params => {:id => 5})

In this case the Hash pointed to by :using_params is merged with the original params. I believe this solves the problems stated in this thread. Let me know if I am missing something. I plan to roll a release in a couple of hours so please respond soon.

cheezy commented 12 years ago

This is released. Please take a look and let me know if all of your issues are resolved.

mgodwin commented 12 years ago

I'm a little late to the party here, but I wonder if it might be possible to change this to be a little more dynamic? My reasoning for this is because you may not always want to specify query string parameters every time you visit a particular URL.

For example, I may want to visit my accounts listing page and specify a limit on how many results are returned, but I may also want to visit it and test the default functionality.

page_url "http://blahblah/accounts?limit=20"
page_url "http://blahblah/accounts"

With the current architecture, the limit=<%=params[:limit]%> will be appended regardless, which isn't really helpful for testing the default.

A solution I was thinking of is to strictly specify query params in the visit_page method, and give the opportunity to specify path params in the page_url definition. So for example if you're trying to access the following url: accounts/52/search?limit=20&query=Find%20this%20query

We could implement that by using the following page_url:

page_url "http://blahblah/accounts/{id}/search"

And the following visit_page call:

visit_page(MyPage, :query_params => {:limit => 20, :query => "Find this query"}, :path_params => {:id => 52})

This approach is very flexible in that it allows for both path and query params, and it makes the query params optional. To add the query params to the url, you can just iterate over the query_params hash and append them to the url during the visit_page call. I think this also keeps your page_urls nice and clean.

What are your thoughts about this approach?

zeljkofilipin commented 11 years ago

I just had to use it, and it works great! Thanks @cheezy :)

cheezy commented 11 years ago

Anything for you Zeljko! I was just afraid you'd kick my ass…

-Cheezy

On Nov 27, 2012, at 4:51 PM, Željko Filipin notifications@github.com wrote:

I just had to use it, and it works great! Thanks @cheezy :)

— Reply to this email directly or view it on GitHub.

zeljkofilipin commented 11 years ago

Why isn't this closed?

knorrium commented 11 years ago

@zeljkofilipin I didn't end up testing the fix, but since you did, feel free to close it :)

zeljkofilipin commented 11 years ago

I can not close the issue that I did not open. :)

lfingerman commented 11 years ago

@cheezy I think your example above has a typo and is supposed to have a class variable @params.

class MyPage include PageObject @params = { :encoding => 'en', :id => 3 } page_url "#{FigNewton.base_url}/somecontext/?id=<%=params[:id]%>?encoding<%=params[:encoding]" end