A Better Shovel

It has been a few months since I have posted anything on my blog and a lot of good things have happened. First of all, I have had 5 releases of the page-object gem since my last post. This post will explain one of the many new features added to page-object.

A better shovel

When writing an acceptance tests against a web application we often find ourselves having to dig through tables to find the data we are interested in. This has typically been done by accessing the rows and columns or perhaps you use one of the Enumerable methods to find your desired result. Let’s build up an example of what this might look like.

Here is the table we will use in the examples that follow:

Col 1 Col 2 Col 3 Col 4
First row ABC DEF GHI JKL
Second row MNO PQR STU VWX
Third row ZY1 234 567 890

For our examples we will use this page-object:

class MyPage
  include PageObject

  table(:report, :id => 'the_table')

  def get_some_information
    # we want to write some code here
  end
end

What we want to do is write some code in the get_some_information method to retrieve the desired values. If we needed to get a value from the second row (with the label ‘First row’ to make it confusing) and the third column (labeled ‘Col 2″) we would write the following:

def get_some_information
  report_element[1][2].text
end

This works well as long as the columns never change and the table layout is constant. But what happens if we need to data from the row labeled “First row” but we don’t know the row number? People would typically use the find method:

def get_some_information
  row = report_element.find { |row| row[0].text == "First row" }
  row[2].text
end

This feels a little better but what if we really need to get the information from the column labeled “Col 2” and we don’t know the column number? Again, we would typically turn to Enumerable:

def get_some_information
  column_number = report_element.first_row.find_index do |col| 
    col.text == "Col 2" 
  end
  row = report_element.find { |row| row[0].text == "First row" }
  row[column_number].text
end

This is starting to look very ugly. The fact is, I was working at a company that had to do this very thing. As I was trying to think of the best solution to make this simpler and more expressive George Shakhnazaryan make a feature request for the gem. He asked for the ability to index rows and columns using a String. When you request a row using a String it would look down the first column for a cell that matched that String and then use that row. When you requested a column using a String it would look across the first row for a cell that matched the String and then use that column. In order to rewrite the last example you would simply do this:

def get_some_information
  report_element['First row']['Col 2'].text
end

This is a much simpler way to dig through tables.

41 thoughts on “A Better Shovel

  1. I’m really missing that kind of tables processing. But for some cases you will need to use iteration still (like if there a multiple rows with the same string in the first column and you need all of them)

    Also, if the provided string is not exist in the table it will raise an exception :

    def get_row(name)
    table_element[name]
    end

    get_row(‘not_exist’)

    TypeError: no implicit conversion from nil to integer
    C:/Ruby/lib/ruby/gems/1.9.1/gems/page-object-0.6.7/lib/page-object/platforms/selenium_webdriver/table.rb:16:in `[]’
    C:/Ruby/lib/ruby/gems/1.9.1/gems/page-object-0.6.7/lib/page-object/platforms/selenium_webdriver/table.rb:16:in `[]’

    Which is not good for a case when a table has a pagination. I’d rather return a nil in this case.

    In any case, thanks for your great work!

    • Please open an issue on the site to make a change so it returns nil when the value is not found. I will make sure it is in the next release.

  2. I’m one of those people that learn by looking at unit tests but in looking at yours there’s one that I don’t see. It’s handling the table when you want to look for the text of the row. I was thinking it would be something like this:


    it "should return a row using text" do
    table_element.stub(:[]).with("row_text").and_return(table_element)
    table_element.should_receive(:rows).and_return(table_element)
    table_element.should_receive(:find_index_by_title).and_return(table_element)
    watir_table["row_text"].should be_instance_of PageObject::Elements::TableRow
    end

    But that clearly doesn’t work. It gets me this:


    Double “table_element” received :[] with unexpected arguments
    expected: (“row_text”)
    got: (#)

    That makes me think I don’t understand how this works. I thought what was supposed to be returned was a TableRow. I’m finding I’m having trouble getting this table logic to work, but I think it’s based on the fact that I can’t get the simple unit test to pass.

    Any idea what I’m doing wrong with the above test?

  3. Pingback: Watir Podcast #48 | Watir Podcast

  4. I’m curious how css selectors are supposed to work. I see your logic but you have a method like this:

    def css_element
    “element(identifier)”
    end

    This is called from

    def call_for_watir_element(identifier, call)
    identifier[:css] ? “#{css_element}” : call
    end

    But all that’s going to do is always make the call this:

    “element(identifier)”

    The identifier part is never being interpolated with the actual identifier, is it? Or am I missing something? I ask this because everytime I try to use css selectors, I get messages like the element doesn’t exist or, more often, needs to be scrolled into view — even when it’s right there on the page and not within a frame or anything else. When I use the exact same selector with a Capybara script, it finds it just fine. So I know I’m using the correct selector.

    • Watir-webdriver only supports css selectors if you use the element method. What I am doing in this case is changing the call. For example, if you accessing a link the original call would be link(identifier) but if you want to use css I need to change that to element(identifier).

      One thing I might mention here. If there are other ways to identify an element (id, class, name, etc) then that is more desirable than css or xpath. css and xpath tend to be brittle.

      • Okay, I get that: makes sense. But in this statement:

        “element(identifier)”

        from your css_element method, this is just a constant string. When does the identifier get interpolated with the actual value of the identifier? To me it looks like you always get a constant string that’s just “element(identifier)”. Or am I wrong on that? Or just totally misunderstanding?

        I would expect something like “element(#{identifier})” to be what’s needed.

        • The string is evaluated in the process_watir_call method. The identifier is in scope so the entire Hash is part of the call. Also note that the identifier has the possibility of changing in the call to parse_identifiers.

  5. Not sure where to ask this question. But I have a question about cell which is link.

    So one of result returns a result table and one of the cells is a link to different page. Is there a way to somehow click a link inside the table cell?When I try to click by id of the cell, it throws NoMethod error

    I know I can define it as a link with xpath as identifier but I don’t want to do that as xpath for the element could change if they add some columns as a part of the result.

    • It is possible to directly declare the link. Here’s an example:

      cell(:the_cell, :id => 'my_id')
      link(:the_link) { |page| page.the_cell_element.link_element }
      

      In this case I’m using a block as the locator. The page is getting passed into the block and I use it to locate the link element. The first thing I am doing is accessing the cell element. Every element in page-object has the complete set of _element methods to handle nested cases. The default parameter for each of these methods is :index => 0. Calling link_element on the cell element will find the first link element inside that cell.

      Hope this helps.

      -Cheezy

  6. Here’s my question: can I have the browser close between scenario execution?

    Here’s the situation:

    When I run two scenarios in one feature file, the first runs fine. The browser closes. Then when the second one tries to run, I get this:

    No connection could be made because the target machine actively refused it. – connect(2) (Errno::ECONNREFUSED)

    I have no firewalls or proxies or anything like that. What I did do, however, was put an After block in features\support\hooks.rb that basically repeats what the at_exit block does.

    So the browser closes for each scenario. This may not even be a problem with how page-object is working, but I’m not sure.

    So I guess my question is this: can I close the browser between each scenario and will page-object reopen it upon starting the new scenario, assuming I have logic in there that instantiates the @browser object?

    What I’m seeing suggests no.

    Both of the scenarios I mentioned above use the page factory. When I change to using a line like this:

    @page = LoginPage.new(@browser)

    then the error I get when trying to run the second scenario (which should open a new browser) is:

    “browser was closed (Watir::Exception::Error)”

    • I would suggest using Before and After hooks to manage creating and closing your browser. The way I typically do this is to create a file named hooks.rb in my support directory and include the following:

      require 'watir-webdriver'
      
      Before do
        @browser = Watir::Browser.new :firefox
      end
      
      After do
        @browser.close
      end
      
      • I was trying to do similar thing
        I have a
        Background:
        Scenario: 1
        Scenario: 2

        So in my scenario 2 i have to close the browser and open it again with data from Background:
        and i am getting this error
        “Errno::ECONNREFUSED: No connection could be made because the target machine actively refused it. – connect(2)”

        Any suggestions for me

  7. It seems to be impossible to find good examples of tables that actually work. So let’s say you have a table that does not have a row name but you have columns. So something like this:

    Account Name | Account Code |
    name 1 | code 1
    name 2 | code 2

    Here the “name 1” and “code 1” stuff is just for illustration. Those names could be lots of different things and what will show up depends on how the table is filtered.

    So what I want to do is cycle through all rows of the table. I want to look in column 2 (called “Account Code”) and see if the row text for that column is some value.

    Will that work with the examples you’ve given here? I ask because I’ve tried it and most of the examples seem predicated upon me knowing what the row contains and I simply won’t.

    What I’m hoping to find is a recipe for: Loop through all rows of a table, finding particular text in a certain column.

    Maybe I’m just not abstracting your examples from above well enough.

  8. Hi Cheezy

    My table is doesn’t have row names or column names, its just data.

    345 |Roger | London
    346 |Adam | New York
    347 |Dorothy | Berlin

    I want to find if my table has “Adam” in Column 2 anywhere. How can I do this using page object?

    I’m only able to access rows right now by
    myTable_each{ |row|
    puts row
    }

    Much appreciated!
    thanks!
    Roger

    • Roger,

      Assuming you defined you table like this:

      table(:my_data, :id => 'blah')
      

      Then you should be able to use any method from Enumerable. For example, the any? method will return true if any entry returns true:

      adam_found = my_data_element.any? { |row| row[1].text == 'Adam' }
      
      • Thanks Cheezy. This is part of my larger question, really.

        I want to enumerate through rows and then columns and print each cell using indexes.

        so in pseudocode
        for i = 1 to table.rows
        for j = 1 to i.columns
        print table[i][j]

        So far, I have this:
        myTable.each {|row|
        }

          • Sorry cheezy, I don’t quite understand.

            1. Let’s say I identified, that one row has ‘Adam’ listed (verified from below)
            adam_found = my_data_element.any? { |row| row[1].text == ‘Adam’ }

            2. How would I access the
            -row number of that row
            -And then print Column 4 of that row?

        • Roger,

          I’m having difficulty answering you because it seems like each posting you are asking a different question. If you are trying to print all of the columns then you would use the code below. If you are trying to print column four for the row that has ‘Adam’ in column two then you would do this:

          row = my_data_element.find { |row| row[1].text == 'Adam' }
          print row[3].text
          

          All of the methods I am demonstrating are a part of Enumerable. Table and TableRow both support Enumerable and it is a good idea to study it.

  9. Hi Cheezy,
    I am running my script in cmd and the test is successfully completed and pass.
    But I am getting this error every time I run the script and I am not able to resolve this.
    It was working fine few days ago, currently it is not working. I am not sure what changes in my script or the configuration has introduced this error.
    Please find the error message below. If you have any questions please ask me.
    Thanks,
    Sanjay
    No connection could be made because the target machine actively refused it. – connect(2) (Errno::ECONNREFUSED)
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:763:in `initialize’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:763:in `open’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:763:in `block in connect’
    C:/Ruby193/lib/ruby/1.9.1/timeout.rb:55:in `timeout’
    C:/Ruby193/lib/ruby/1.9.1/timeout.rb:100:in `timeout’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:763:in `connect’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:756:in `do_start’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:745:in `start’
    C:/Ruby193/lib/ruby/1.9.1/net/http.rb:1285:in `request’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/http/default.rb:83:in
    response_for’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/http/default.rb:39:in
    request’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/http/common.rb:40:in
    all’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/bridge.rb:629:in `raw
    xecute’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/bridge.rb:607:in `exe
    te’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/remote/bridge.rb:356:in `del
    eAllCookies’
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/selenium-webdriver-2.33.0/lib/selenium/webdriver/common/options.rb:67:in `del
    e_all_cookies’
    C:/WorkingDirectory/AvivaHousehold/stories/features/support/env.rb:34:in `After’

    iling Scenarios:
    cumber features\payment_page.feature:68

    scenario (1 failed)

      • # After each scenario
        After do |scenario|
        @test_context = nil
        @browser.driver.manage.delete_all_cookies
        if scenario.failed?
        encoded_img = @browser.driver.screenshot_as(:png)
        @browser.driver.save_screenshot(create_screen_shot_location(scenario))
        embed(“data:image/png;base64,#{encoded_img}”, ‘image/png’)
        end
        test_results = ResultsMapper.store_results(scenario, test_results)
        end

        # “after all”
        at_exit do
        ResultsWriter.new(test_results).write_xml_file(File.join(File.dirname(__FILE__), ‘../../ruby_framework/test_report/results.xml’))
        if File.exist?(File.join(File.dirname(__FILE__), ‘../../manual_tests.yml’) ManualTestsWriter.new.write_xml_file(File.join(File.dirname(__FILE__), ‘../../ruby_framework/test_report/manual_tests.xml’))
        end
        browser.quit
        end

      • Also my hooks.rb looks like this which you have mentioned in your above post
        =============
        require ‘watir-webdriver’
        Before do
        @browser = Watir::Browser.new :firefox
        end
        After do
        @browser.close
        end

        • My guess is that the After block in hooks.rb is getting executed first which is closing the browser. When the After block in env.rb is executed the @browser instance variable is no longer to communicate with the browser. Try to combine the After blocks into one. This should fix the issue.

          • It was working fine before.
            Combining after blocks into one? how do I do that?
            Can you please combine for me ?
            Thanks,
            Sanjay

    • No – it is closing it in the after block in your hooks.rb. That is getting executed first and then the second After block is failing when it tries to call a method on the @browser. Update the after block in hooks.rb to this:

      After do |scenario|
      @test_context = nil
      @browser.driver.manage.delete_all_cookies
      if scenario.failed?
      encoded_img = @browser.driver.screenshot_as(:png)
      @browser.driver.save_screenshot(create_screen_shot_location(scenario))
      embed(“data:image/png;base64,#{encoded_img}”, ‘image/png’)
      end
      test_results = ResultsMapper.store_results(scenario, test_results)
      @browser.close
      end

      And remove the after block in env.rb.

  10. I updated the hook.rb as you said –

    I removed –
    After do
    @browser.close
    end

    AND

    After do |scenario|
    @test_context = nil
    @browser.driver.manage.delete_all_cookies
    if scenario.failed?
    encoded_img = @browser.driver.screenshot_as(:png)
    @browser.driver.save_screenshot(create_screen_shot_location(scenario))
    embed(“data:image/png;base64,#{encoded_img}”, ‘image/png’)
    end
    test_results = ResultsMapper.store_results(scenario, test_results)
    @browser.close
    end
    Now the tests are running fine but the browser is not closing after each test

    Also, after each test run I am see a message in cmd prompt –

    undefined method `has_key?’ for nil:NilClass (NoMethodError)
    C:/WorkingDirectory/AvivaHousehold/stories/ruby_framework/scenario/results_mapper.rb:14:in `store_results’
    C:/WorkingDirectory/AvivaHousehold/stories/features/support/hooks.rb:19:in `After’
    ==================
    My results_mapper.rb looks like this –

    require ‘active_support/inflector’
    class ResultsMapper
    def self.store_results(scenario, result_store)
    result = scenario.passed? ? “passed” :”failed”
    if scenario.respond_to? :feature
    feature_name = scenario.feature.file.sub(“features\\”, “”).gsub(“_”, ” “).titleize
    scenario_name = scenario.to_sexp[3].gsub(/[^\w\-]/, ‘ ‘)
    elsif scenario.respond_to? :scenario_outline
    feature_name = scenario.scenario_outline.feature.file.sub(“features\\”, “”).gsub(“_”, ” “).titleize
    scenario_name = “#{scenario.scenario_outline.title}:#{scenario.to_hash[“Scenario Title”]}”
    end
    if not result_store.has_key?(feature_name)
    result_store[“#{feature_name}”]= [{“#{scenario_name}” => “#{result}”}]
    else
    result_store[“#{feature_name}”] < “#{result}”}
    end
    result_store
    end
    end

    Can you please help. I think we are almost there. I’ve been struggling with this.

    Thanks,
    Sanjay

  11. Just addition to my above comment –

    I removed –
    After do
    @browser.close
    end

    AND ADDED TO hooks.rb

    After do |scenario|
    @test_context = nil
    @browser.driver.manage.delete_all_cookies
    if scenario.failed?
    encoded_img = @browser.driver.screenshot_as(:png)
    @browser.driver.save_screenshot(create_screen_shot_location(scenario))
    embed(“data:image/png;base64,#{encoded_img}”, ‘image/png’)
    end
    test_results = ResultsMapper.store_results(scenario, test_results)
    @browser.close
    end

  12. Hi Cheezy,
    Thanks for giving me the right direction on fixing the above issue.
    Actually I commented out all the lines in hooks.rb. That was the only change I made.
    And my scripts are running now without no errors.

    Thanks,
    Sanjay 🙂

  13. Hi Cheezy,
    How can I store a value to be verified when the values keep changing dynamically.
    e.g. Scenario: Selecting Gold Product and view product details on payment page
    In my scenario I have to verify the price of a product when I land on products page.
    But the price keeps changing dynamically. So I need to store the price when I select the product and when I land on payment page, I need to verify that price. How can I do that ?
    Can you please suggest me.
    Thanks,
    Sanjay

    • Hey Sanjay and Cheezy I am running into this same issue. I am looking for a possible solution. My situation is we have one page that lists all the gift certificate numbers than on another page i need to use that gift cert number to checkout. Part of my route looks like this. Is it possible to extract a value in my first step (get_gift_cert_number) than use that value in my second step (checkout_with_gift_cert) if they are on different pages?

      :redeem_gift_cert => [
      [ManageGiftCertificatesPage, :get_gift_cert_number],
      [PaymentPage, :checkout_with_gift_cert],
      ],

  14. Hi cheezy,
    I have a table like this

    title | value
    campaign | 5
    test | yes/no*

    *yes/no is a select list

    to get retrieve the value 5 i used the following code which was successful

    test code :
    @user.return_admin_advertiser_table_value(‘campaign’))

    user code:
    def return_admin_advertiser_table_value(row)
    on(AdminPage).admin_advertiser_table_element[row][‘value’].text
    end

    page code:
    table(:admin_advertiser_table, :class => ‘dataTable’)

    however I’m trying to alter the value inside the select list and it will not work, I tried to model it on the answer you gave above for links

    this is my code

    test code:
    @user.select_value_from_table_drop_down(‘test’, ‘No’)

    user code:
    cell = on(AdminPage).admin_advertiser_table_element[row][‘Value’]
    list = on(AdminPage).convert_cell_into_drop_down(cell)
    list = ‘No’

    page code:
    def convert_cell_into_drop_down(cell)
    select_list(:drop_down) {|page| page.cell_element.select_list_element}
    end

    the error produced is
    NoMethodError: undefined method `select_list’ for #

    • Charlie,

      There are several problems with the code here. Let me give you some background first. In the page-object gem there are two types of methods that can be used to access elements on your pages. The first type will generate methods on a page-object class that you can use to interact with the element. select_list is an example of this. The second type will access the select list directly and not generate any methods. select_list_element is an example of this second type.

      You should be able to simply change the method you have to this and it should work:

      def convert_cell_into_drop_down(cell_element)
        cell_element.select_list_element
      end
      

      The second thing I would point out is that you have a lot of code in your user code that knows about the page. It would be better to move this code into the page object and have your user code know nothing about the structure of the page. This, after all, is the primary purpose for the page-object gem.

      -Cheezy

      • Cheezy

        Thanks for your help and advice,

        That is working and returning the select_list object correctly, I’m struggling to get the text from the selected option though

        calling .text on it returns every possible options text as a string, while .value returns the selected option, but the value in the html tag not the actual text, is there a clean way to retrieve the text?

        thanks

        charlie

      • one more question , i have a similar situation to the original one i mentioned, but now instead of a select list it is a text field,
        should the following code work

        def convert_cell_into_drop_down(cell_element)
        cell_element.text_field_element
        end

        I’m using exactly the same process for the select list, and the cell element is definitely being identified, however this will throw a no such element exception

Leave a Reply

Your email address will not be published. Required fields are marked *