Those pesky frames and iframes

Wouldn’t it be sweet if all of the web pages in the world were nicely formed with easy to identify elements. If you work in an environment like this, I am envious. I often find myself working with a team where the sites are not pristine (I’m being nice here).

One thing I have found difficult to work with is pages that have frames and iframes. It gets worse when the elements you are trying to work with are nested within multiple frames/iframes. To address this complexity I decided to add simple frames handling to page-object.

The page-object gem provides a way to declare elements are within frames or iframes. In fact, these frames can be nested infinitely. Let’s look at a simple case.

class RegistrationPage
  include PageObject

  in_frame(:id => 'left-frame') do |frame|
    text_field(:address, :id => 'address_id', :frame => frame)
  end
end

In this example we are declaring that the textfield exists within a frame identified by the id ‘left-frame’. Note that I am passing the frame argument for the block as the last parameter to text_field. In the remainder of my class I can just refer to the element by its’ name – in this case ‘address’.

Frames can be identified by :name, :id, or :index and frames and iframes are handled identically.

Let’s take a look at how we would handle a situation where the frames (or iframes) are nested within other frames. We’ll modify the example above.

class RegistrationPage
  include PageObject

  in_frame(:id => 'left-frame') do |frame|
    in_frame({:id => 'left-top-frame'}, frame) do |frame|
      text_field(:address, :id => 'address_id', :frame => frame)
    end
  end
end

There are a couple of interesting things here. First of all we just nested our previous call to in_frame in another call to the same method. Also note that we had to pass the frame block parameter from the first call to the second call to in_frame. This method is how page-object keeps track of the frame nesting and allows you to nest calls to in_frame as many times as necessary.

That’s all there is to it. Enjoy.

26 thoughts on “Those pesky frames and iframes

    • Rusty Robot,

      You just declare the elements that are within the frame once in your class. Use the examples above as your guide. With this declaration you can just use the generated methods elsewhere to get/set the value and interact with the element. For example, the snippets above will create the methods to access the info. They are:


      address
      address=
      address_element

  1. Okay. One more bit of help, if you don’t mind.

    I’m having trouble with your gem’s required syntax for defining this sort of page object (defined below using Watir syntax):

    browser.frame(:index=>1).div(:class=>”portletBody”).div(:class=>”alertMessage”)

    I’ve got the in_frame part down, but haven’t been able to figure out how to set up the nested divs part.

    • Abe,

      Can you provide a little more information about the html? If there is only one ‘alertMessage’ div you can access it directly using this declaration:


      in_frame(:index => 1) do |frame|
      div(:alert_message, :class => 'alertMessage')
      end

      If you need to access the ‘alertMessage’ that is contained within the ‘portletBody’ specifically because there are others on the page you can use this:


      in_frame(:index => 1) do |frame|
      div(:alert_message) { |page| page.div_element(:class => 'portletBody').div_element(:class => 'alertMessage') }
      end

      Hope this helps
      -cheezy

  2. Cheezy,

    I need yet more help with syntax. How do you set up a reference to an element that requires identification via two attributes?

    Example:

    I have some radio buttons that all have the same id, but different values. Then there are other radio buttons with different ids, but the same values as the other radio buttons. So I need to specify them all using both :value and :id tags. To complicate things further, they’re all inside an iframe.

    Here’s what an identifier would look like in Watir:

    @browser.frame(:index=>1).radio(:value=>”true”, :id=>”enable”)

    I’ve tried enclosing things in curly braces, but I just get a “wrong number of arguments” error.

    I’ve looked all over your documentation to see if you already have an example of how to do this, but couldn’t find it. Sorry if I just missed it.

  3. Ah. I just discovered in your documentation for the radio_button_element element locator that :value is not a valid key. 🙁

    I guess I’ll be adding the request to your issues list.

  4. And, BTW, I figured out the answer to my question, thanks to your hints above and the discovery that I can’t use the :value key:

    Here’s an example that works for me…
    in_frame(index=>1) do |frame|
    radio_button(:create_using_text) { |page| page.radio_button_element(:name=>"authorIndexForm:_id29", :index=>1, :frame=>frame) }
    end

    Thanks!

  5. Cheezy,

    I’ve got another one for you! 🙂

    The iframes of the pages that I’m testing are a real pain. Their names and ids are dynamically generated and so will be different pretty much every time a page gets tested (I’ve asked for testability improvements, here, but the project is open-source and is nearing the end of its life, so it’s doubtful that I’ll get my request fulfilled).s

    The best I’ve been able to do is use a frame’s index value. However, this index value can change in certain contexts.

    I thought that I could account for this in Page Objects like this:

    in_frame(:index=>$frame_index) do |frame|
    bla bla
    end

    However, based on the errors I'm getting it seems like all of the page object methods get created as soon as the script starts up, rather than whenever the particular Class object gets instantiated, because the $frame_index value for the pages where I see the error is always set to whatever $frame_index was initialized as.

    Is that correct? If so, then is there any way to get page-objects to create the Class's object methods only when the Class is instantiated?

    OTOH, maybe there's something else I'm doing wrong and I'm misinterpreting what the error stack is telling me.

    Thanks in advance for any help you can provide.

    • Abe,

      The way the class level method work is by creating a set of new methods as they are executed (when the class is loaded). There are three other ways to find elements on the page. They are:

      1) There are a set of methods on the PageObject module that allow you to find things dynamically. All of these methods take the form of _element. For example, if you need to find a div based on an index that is only known at runtime you could do div_element(:index => the_index_value).

      2) If you need to find an element within the context of another element you can use a set of _element methods that exist on all elements. For example, to find an unordered_list within a div you can make this call on the div -> the_div.unordered_list_element(:id => 'the_id').

      3) If you can statically define something within the context of another element you can do this at the class level


      div(:error_div, :id => 'error_explanation')
      unordered_list(:errors) { |page| error_div_element.unordered_list_element(:name => 'some_name') }

      The problem is that I haven’t extended any of this behavior to frames. Please open an issue and I’ll try to get to it in the next release.

      -Cheezy

      • Thanks!

        I’m just thankful that the number of pages with this problem is relatively small.

        And, thankfully, the next project I’ll be working on does not use any frames on the pages. Whew!

        • I’ve just added the ability to nest instance calls inside in_frame calls. I am hoping to include it in a 0.5.1 release within the next day or two. Here is how it can be used:

          in_frame(:id => 'frame_1') do |frame|
          text_field_element(:id => 'my_id', :frame => frame).set("some value")
          end

  6. I see how this is supposed to work when you declare the page object. So I have something like this:

    class CustomerPage
    in_frame(:id => “form”) do |frame|
    text_field :customer_value, :id => “customerValue”, :frame => frame
    end
    end

    But then how do I actually reference that customer_value from a step definition?

    @page.customer_value doesn’t work.

    I then tried something like this in the step def:

    @page.in_frame(:id => “form”) do |frame|
    @page.customer_value = text
    @text = @page.customer_value
    end

    But that got me the following error:

    undefined method `in_frame’ for # (NoMethodError)

    Unsure of what to do. I think the main thing is that it’s not clear to me how to use this in step definitions; it’s very clear how to declare it.

    • A text_field generates three methods. They can all be called from the step definitions. They are:


      def customer_value # returns the value from the text_field
      def customer_value= # sets the value in the text_field
      def customer_value_element # returns the actual text_field element

  7. I’m trying to understand how this in_frame stuff works since I have a series of issues with it. In your code base, you have in_frame declared in four places:

    The two page_object.rb files (under watir_webdriver and selenium_webdriver) make sense. Those are the specific implementations of how frames are recognized for each driver.

    But you have what appear to be identical methods called in_frame. One is in page_object.rb (the main one in the lib directory) and the others is in the accessors.rb file. The former makes sense: it’s calling the platform-specific ones mentioned above. So when is the one in accessors.rb called?

    • Steve,

      There are actually two public in_frame methods. The one in accessors.rb is the one that is available at a class level. It is used to declare that elements will be found in a frame when you call the generated methods for the element. The method in page_object is an instance level method that can be used to identify items found in the frame at runtime. The second one can also be used with the Javascript popups if they are attached to a DOM from a frame. Hope this helps.

      -Cheezy

  8. I am trying to pattern my code with the examples here but continue to be frustrated in getting to in the following html snippet


    .
    .
    .

    .
    .
    .

    .
    .
    .

    This is the code i am using
    in_frame(:id => ‘y’) do |frame|
    in_frame({:id => ‘Two’}, frame) do |frame|
    in_frame({:id => ‘abc’}, frame) do |frame|
    div(:sys_info, :class => “TblMgmt”, :frame => frame)
    end
    end
    end

    sys_info_element.when_present is timing out although I can see the page. Changing the timeout didn’t help which leads me to think that my locator is incorrect. Any help would be much appreciated

  9. i am trying to user page objects but i have run into a problem where i am trying to populate a text_field inside nested frames. I am trying the following

    in_frame(:id => 'frameMain') do |frame|
         in_frame({:id => 'nv-iframe-183'}, frame) do |frame|
    	text_field(:name, :id => 'settings_user_name', :frame => frame )
         end
    end
    

    but i am getting the following error

    Element belongs to a different frame than the current one – switch to its containing frame to use it
    Using irb I can access the element through
    browser.frame(:id, ‘frameMain’).frame(:id,’nv-iframe-183′).table(:id,’mainContent’).table(:id,’main_form’).text_field(:id,’settings_user_name’).click

    I have tried accessing the text_field through .element as well but that does not seem to work as well. Any help would be highly appreciated.

    thanks,

    Saba

  10. Cheezy, is this a bug?

    I have this line:


    in_frame(:id=>/_frame/) do |f|

    When I run the script I get this error:


    unable to locate frame/iframe using {:id=>"(?-mix:_frame)"}

    Lemme know if I need to submit this in Github--or tell me what I'm doing wrong. Thanks!

  11. Hi Cheezy,

    I have been looking for some time but I have not found a way to handle the iframe itself (not its inner elements) with your gem. Would it be possible to add accessors for a frame/iframe element so we can actually switch to that frame?

    Thanks!

  12. Hi Cheezy,

    As long as I use the in_frame method I should get access to my page, right?

    I am having a lot of trouble with this, I have a full page that is wrapped by an iFrame, its such a pain to test.

    I seem to be able to get the content (i.e. text) and click elements within the frame but I cannot get hold of elements to assert that they are present.

    For example in my object I have:

    in_frame(:id => ‘master’) do |frame|
    button(:apply_now_button, :id => ‘apply_now_button’, :frame => frame)
    end

    def apply_now_element
    apply_now_button_element
    end

    And in my step definition I would like to assert that this apply_now_button_element is present. However when I do, I get the following error:
    Element belongs to a different frame than the current one – switch to its containing frame to use it (Selenium::WebDriver::Error::StaleElementReferenceError)

    Any ideas?

    -Thanks

Leave a Reply

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