Skip to content

Defining Page Objects

jfitisoff edited this page Aug 6, 2018 · 1 revision

You can start writing page objects after you've defined a site class for your site.

Sample page object definition

# This is a very simple example. It assumes that you've created a site 
# class called "MySite." 

# 1.)
# Your pages should inherit from your site's Page class. You don't need
# to define this class for your site, it's automatically created when
# you include the module.
class LoginPage < MySite::Page
  # 2.)
  # Define a relative URL for the page (or a complete URL.) Most of the
  # time you'll want to define a relative URL. The string provided here
  # is used to create a URL template. See below for more information.
  set_url "/login"

  # 3.)
  # Define accessor methods for your page elements.
  text_field :email,         id: "email"
  text_field :password,      id: "pwd"
  button     :log_in_button, type: "submit"

  # 4.)
  # Define higher-level methods that use these page elements. See below
  # for more info about the update_page method and updating pages.
  def log_in(hsh = {})
    update_page hsh
    log_in_button.click
  end
end

Setting a page's URL

When defining a page URL using the set_url method, you can:

  • Specify a RELATIVE URL. This is the most common use case. When a relative URL is specified, the base URL (provided as an argument when you create a new site instance) and the relative URL are concatenated together to form a complete URL
  • Provide a FULL URL. In this case, the base URL is ignored.
  • Don't specify a URL. In this case, the base URL will be used (Generally you almost never want to do this.)

URL templates

Whatever option you use, it'll result in a URL template getting created for the page. Insite uses URL templates to build page URLs and (mostly) to identify whether the browser's URL is correct or not for a given page. "URL template" isn't a conversational term, it's an actual thing with an RFC. Insite uses addressable to implement URL templates for its page objects. Templates allow you to build (and identify) complex URLs but they have their own set of rules and it's definitely worth mentioning URL templates here because a lot of people aren't familiar with them.

Embedded variables

Let's say that you are testing an health club app. This app allows you to create, edit and view club members. The URLs for these pages look like this:

  • /members (Summary page that shows all members.)
  • /members/:member_code (Details page for a single member, :member_code is a code that uniquely identifies a member.)
  • /members/:member_code/edit (Edit page for an individual member.)

The member_code (which will be different for each member) can be handled in the page's URL definition like this:

# IMPORTANT: String interpolation isn't being used here (so no pound sign.)
set_url "/members/{member_code}"

As mentioned above, Ruby string interpolation isn't being used here. This is because URL templates don't do immediate string interpolation. You must omit the leading pound sign when defining you're embedded variables in order for your templates to work. When defining URL variables, just use curly brackets without the pound sign.

If the template for your page includes embedded variables then you pass them in as hash arguments like this:

# Access the details page for club member with member code 12345:
s.member_details_page(member_code: 12345)

In the example above, a member_code is being passed in as a hash argument. But insite also does some additional work when some non-hash object is passed in. It will query the object to see if it responds to a method call that corresponds to the argument name (duck typing.) This simplifies page method calls.

Let's say that there's an API object or an ActiveRecord DB object for the health club member.

# member is some Ruby object that responds to member_code:
s.member_details_page(member)

# No need to do something like this:
s.member_details_page(member_code: member.member_code)

Technical stuff: Hey, will there be any URL template matching problems?

Short answer is that there shouldn't be.

This is because insite has this idea of a site object and the site object knows about all of its pages (and the templates that were defined for them.) This makes it easier for the library to do a little bit of extra work to ensure that the right page is matched when the site's context changes.

A little more background: Both of these templates:

  • "/members/{member_code}"
  • "/members/{member_code}/edit"

Will return a match for the following URL string:

"/members/12345/edit"

Which could result in a situation like this (but shouldn't with insite):

# Both of these shouldn't be true but they are (because one
# of the template matches isn't specific enough.)
member_page.on_page?
=> true

member_edit_page.on_page?
=> true

If you see something like this happening with insite it should be reported as a bug (if the page templates for the pages in question look OK.)

URL matchers (deprecated)

To override the default template URL matching you can define a URL matcher. The URL matcher is a regular expression that's used to match the URL in place of the template. It completely replaces the URL template when it comes to identifying pages:

class MemberEditPage < MySite::Page
  set_url "/members/{member_code}/edit"
  set_url_matcher %r{/members/\d+/edit}
end

Defining page element and component accessors

Each standard DOM element has a corresponding method for defining page accessors in page objects. Insite element classes are wrappers around their corresponding watir classes. Here's how you define accessor methods:

# [DOM TYPE]  [METHOD NAME]   [HOW, WHAT (HASH ARGS)]
  div         :customer_info  id: "cinfo", text /^Customer/

# [DOM TYPE]  [METHOD NAME]   [HOW, WHAT (NON-HASH ARGS, SINGLE IDENTIFIER)]
  div         :customer_info  :id, "cinfo"

Page component accessors

Components work the same way. If you've defined components, there will be two methods available to add them when defining page objects. One method will be for an individual instance of the component and another represents a collection.

The method names will be the snake case version of the component's class name (or the pluralized snake case version of the class name for the collection case):

class FooWidget < MySite::Component
end

class UICard < MySite::Component
end

class ExamplePage < Mysite::Page
  set_url "/example"

  # Accessor method for instance of FooWidget.
  foo_widget :my_foo_widget, text: 'My Foo Widget Title'

  # Accessor method for a single UICard
  ui_card  :first_card, index: 0

  # Accessor method for all UI cards on the page. But it's worth noting
  # here that ui_card and ui_cards accessor methods automatically got
  # defined for all pages when the component was created, so you might
  # not want to even bother manually creating an accessor method in this
  # case.
  ui_cards :cards
end