{"id":19339376,"url":"https://github.com/appfolio/ae_page_objects","last_synced_at":"2025-04-12T22:55:00.676Z","repository":{"id":5884205,"uuid":"7102043","full_name":"appfolio/ae_page_objects","owner":"appfolio","description":"Page Objects for Capybara","archived":false,"fork":false,"pushed_at":"2024-06-25T20:27:32.000Z","size":3479,"stargazers_count":28,"open_issues_count":20,"forks_count":9,"subscribers_count":142,"default_branch":"master","last_synced_at":"2025-04-12T22:54:56.675Z","etag":null,"topics":["gem"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/appfolio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2012-12-10T22:34:14.000Z","updated_at":"2024-06-25T20:27:35.000Z","dependencies_parsed_at":"2023-09-27T09:11:51.847Z","dependency_job_id":"2f77fff7-f901-4c15-823d-ae39de35fa27","html_url":"https://github.com/appfolio/ae_page_objects","commit_stats":{"total_commits":491,"total_committers":26,"mean_commits":"18.884615384615383","dds":"0.32179226069246436","last_synced_commit":"8cd0aa148f410fac17b5b99fa1fcc2478e3d146f"},"previous_names":[],"tags_count":76,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfolio%2Fae_page_objects","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfolio%2Fae_page_objects/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfolio%2Fae_page_objects/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfolio%2Fae_page_objects/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/appfolio","download_url":"https://codeload.github.com/appfolio/ae_page_objects/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248643048,"owners_count":21138353,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gem"],"created_at":"2024-11-10T03:21:50.394Z","updated_at":"2025-04-12T22:55:00.647Z","avatar_url":"https://github.com/appfolio.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AePageObjects\n\n_Page Objects for Capybara_\n\n[![Gem Version](https://badge.fury.io/rb/ae_page_objects.png)](http://badge.fury.io/rb/ae_page_objects)\n[![Build Status](https://api.travis-ci.org/appfolio/ae_page_objects.png?branch=master)](http://travis-ci.org/appfolio/ae_page_objects)\n[![Code Climate](https://codeclimate.com/github/appfolio/ae_page_objects.png)](https://codeclimate.com/github/appfolio/ae_page_objects)\n\nAePageObjects provides a powerful and customizable implementation of the Page Object pattern built on top of Capybara to\nbe used in automated acceptance test suites.\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n- [Overview](#overview)\n- [Setup](#setup)\n  - [Rails](#rails)\n  - [Non Rails](#non-rails)\n- [Object Model](#object-model)\n- [Documents](#documents)\n  - [Creating a Document](#creating-a-document)\n  - [Adding a Path](#adding-a-path)\n  - [Navigation](#navigation)\n  - [Load Ensuring](#load-ensuring)\n  - [Customizing Load Ensuring](#customizing-load-ensuring)\n  - [Windows](#windows)\n  - [Multiple Windows](#multiple-windows)\n  - [Conventions](#conventions)\n    - [Variable Results](#variable-results)\n- [Elements](#elements)\n  - [Defining Elements](#defining-elements)\n  - [Creating elements on the fly](#creating-elements-on-the-fly)\n  - [Nested Elements](#nested-elements)\n    - [Extending Nested Elements](#extending-nested-elements)\n  - [Custom Elements](#custom-elements)\n  - [Forms](#forms)\n  - [Collections](#collections)\n    - [Custom Collections](#custom-collections)\n  - [Staling](#staling)\n  - [Load Ensuring](#load-ensuring-1)\n  - [Checking presence](#checking-presence)\n  - [Waiting for presence](#waiting-for-presence)\n  - [Checking visibility](#checking-visibility)\n  - [Waiting for visibility](#waiting-for-visibility)\n  - [Locators](#locators)\n    - [Default Locator](#default-locator)\n- [Router](#router)\n  - [Configuration](#configuration)\n    - [Router interface](#router-interface)\n    - [Configure default router](#configure-default-router)\n    - [Configure router per document](#configure-router-per-document)\n  - [Sharing routers across groups of documents](#sharing-routers-across-groups-of-documents)\n- [Development](#development)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Overview\n\nDescribe the pages of your site by writing Ruby classes.\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n\n  # use Rails URL helpers\n  path :new_user_session\n\n  form_for :user do\n    element :email\n    element :password\n  end\n\n  def login!(username, password)\n    email.set username\n    password.set password\n\n    node.click_on(\"Log In\")\n\n    window.change_to(AuthorsIndexPage)\n  end\nend\n\nclass AuthorsIndexPage \u003c AePageObjects::Document\n  path :authors\n\n  collection :authors,\n             is:           Table,\n             locator:      \"table\",\n             item_locator: \"tr\" do\n\n    element :first_name, locator: '.first_name'\n    element :last_name,  locator: '.last_name'\n\n    def show!\n      node.click_link(\"Show\")\n\n      window.change_to(AuthorsShowPage)\n    end\n  end\nend\n\nclass AuthorsShowPage \u003c AePageObjects::Document\n  path :author\n\n  element :first_name\n  element :last_name\nend\n```\n\nUse the page objects in a test\n\n```ruby\ndef test_logging_in_goes_to_authors\n  login_page = LoginPage.visit\n  authors_page = login_page.login!('admin', 'password')\n\n  assert_equal AuthorsIndexPage, authors_page.class\nend\n\ndef test_authors_are_sorted_by_last_name\n  Author.create!(first_name: 'Bob', last_name: 'Smith')\n  Author.create!(first_name: 'Sponge', last_name: 'Bob')\n\n  authors_page = LoginPage.visit.login!('admin', 'password')\n\n  authors = authors_page.authors\n  assert_equal 2, authors.size\n\n  sponge_bob = authors.first\n  assert_equal \"Sponge\", sponge_bob.first_name.text\n  assert_equal \"Bob\", sponge_bob.last_name.text\n\n  bob_smith = authors.last\n  assert_equal \"Bob\", bob_smith.first_name.text\n  assert_equal \"Smith\", bob_smith.last_name.text\nend\n\ndef test_can_navigate_to_author_from_index\n  Author.create!(first_name: 'Sponge', last_name: 'Bob')\n\n  authors_page = LoginPage.visit.login!('admin', 'password')\n\n  sponge_bob = authors_page.authors.first\n  sponge_bob_page = sponge_bob.show!\n\n  assert_equal \"Sponge\", sponge_bob_page.first_name.text\n  assert_equal \"Bob\", sponge_bob_page.last_name.text\nend\n\ndef test_can_navigate_to_author_after_logging_in\n  sponge_bob = Author.create!(first_name: 'Sponge', last_name: 'Bob')\n\n  LoginPage.visit.login!('admin', 'password')\n\n  sponge_bob_page = AuthorsShowPage.visit(sponge_bob)\n\n  assert_equal \"Sponge\", sponge_bob_page.first_name.text\n  assert_equal \"Bob\", sponge_bob_page.last_name.text\nend\n\n```\n\n## Setup\n\nAePageObjects is built to work with any Ruby project using Capybara. To install, add ae_page_objects to your Gemfile:\n\n```ruby\ngem 'ae_page_objects'\n```\n\n### Rails\n\nAePageObjects is built to work with Rails out of the box. To use with Rails,\nadd this line to your test helper:\n\n```ruby\nrequire 'ae_page_objects/rails'\n```\n\n### Non Rails\n\nAePageObjects works in non-Rails environments out of the box. However, you'll probably want to configure a custom Router.\nSee [Router](#router) for information.\n\n## Object Model\nAePageObjects mirrors the internal design of Capybara's Node hierarchy, whereby:\n\n```\nAePageObjects                   Capybara\n--------------------            --------------------\nNode                            Node::Base\nElement \u003c Node                  Node::Element \u003c Node::Base\nDocument \u003c Node                 Node::Document \u003c Node::Base\n```\n\n`AePageObjects::Node` holds a reference (`node`) to the underlying `Capybara::Node::Base`. For\n`AePageObjects::Document` the underlying Capybara node is a `Capybara::Node::Document` and for\n`AePageObjects::Element` the underlying Capybara node is a `Capybara::Node::Element`. Additionally,\njust like in Capybara, every `AePageObjects::Element` has a reference to its parent node. Below is a\nUML-ish model detailing the relationships.\n\n\n```\n                 AePageObjects                .                      Capybara\n\n                                              .\n                .----------.\n                |          |         node     .\n      ,---------|   Node   |\u003c\u003e---------------------------------------------.\n      |         |          |                  .                            |\n      |         `----------'                                          .----------.\n      |          ^       ^                    .                       |          |\n      |          |       |                                  ,---------|   Node   |\n      |          |       |                    .             |         |          |\nparent|          |       |                                  |         `----------'\n      |          |       |                    .             |          ^       ^\n      |          |       |                                  |          |       |\n      |    .---------.  .----------.          .             |          |       |\n      |    |         |  |          |                  parent|          |       |\n      `--\u003c\u003e| Element |  | Document |          .             |          |       |\n           |         |  |          |                        |          |       |\n           `---------'  `----------'          .             |    .---------.  .----------.\n                                                            |    |         |  |          |\n                                              .             `--\u003c\u003e| Element |  | Document |\n                                                                 |         |  |          |\n                                              .                  `---------'  `----------'\n```\n\n## Documents\n\nIn AePageObjects web pages are represented by `AePageObjects::Document`. This section describes how to create and use\ndocuments.\n\n### Creating a Document\n\nTo create a Document, simply subclass `AePageObjects::Document`:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\nend\n```\n\n### Adding a Path\n\nMost pages on your site will be reachable via a URL. With AePageObjects you can specify the path to your page via the\n`path` method:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\nend\n```\n\nThe type of arguments that `path` can take depends on the configured router. For Rails projects, `path` will accept\nstrings and Rails URL helper names. See [Router](#router) for more details.\n\n\n### Navigation\n\nOnce a path is specified on a document, you can use the document to direct the browser to the page via the `visit`\nclass method:\n\n```ruby\nlogin_page = LoginPage.visit\n```\n\n`visit` navigates the browser to the page and then returns an instance of the document representing the page.\n\nIf a  page can be visited by multiple paths. You can use  the `:via` option to specify which path to use. For example:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_session\n  path :access_autologin\nend\n\n# navigates to /session/new\nlogin_page = LoginPage.visit\n\n# navigates to \"/autologin/access?token=#{token}\"\nlogin_page = LoginPage.visit(token: token, via: :access_autologin)\n```\n\nThe `visit` method will pass down arguments to the configured router (excluding `via:`). In Rails projects, this means\nyou can pass in the same arguments you would pass to Rails URL helpers.\n\n### Load Ensuring\n\nWhen instantiating a `AePageObjects::Document`, AePageObjects verifies that the `AePageObjects::Document` matches the\ncurrent page in the web browser. This process is called \"load ensuring\". For documents, the default load ensuring\nmechanism verifies that the page in the browser matches the path of the document. For example, given:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\nend\n```\n\nIf the browser is currently at `http://yoursite.com/users/sign_in`, the following will work:\n\n```ruby\nlogin_page = LoginPage.new\n```\n\nHowever, if the browser is not at that URL, say `http://yoursite.com/dashboard/statistics`, then instantiating the page object\nwill fail:\n\n```ruby\nlogin_page = LoginPage.new\nAePageObjects::LoadingPageFailed: LoginPage cannot be loaded with url '/dashboard/statistics'\n  test/selenium/login_test.rb:16:in `new'\n```\n\nSometimes pages can be loaded from multiple paths. `path` can be called multiple times to specify additional paths:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\n  path :login\n  path '/new_user/login'\nend\n```\n\nLoad ensuring will allow the document to be instantiated if the browser's URL matches any of these paths. The `visit`\nmethod described in [Navigation](#navigation) will always use the value passed to the first call to `path`.\n\n### Customizing Load Ensuring\n\nSometimes your page's DOM loads quick, but there is significant time spent after the DOM has been loaded in JavaScript\nbefore your page is actually usable. AePageObjects' load ensuring can be used to wait for the page to be ready before\nreturning control to the users of the page being loaded.\n\nThere are two options:\n\n- You can override the `loaded_locator` method to return a locator ([Locators](#locators)) that will be looked for after the URL check is made:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\n  path :login\n  path '/new_user/login'\n\nprivate\n  def loaded_locator\n    [:css, \".something .somewhere\", {visible: true}]\n  end\nend\n```\n\n- You can use `is_loaded` to implement any type of checks:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\n  path :login\n  path '/new_user/login'\n\n  is_loaded { current_url =~ /\\?debug\\=true/ }\nend\n```\n\nThe `is_loaded` block is meant to return quickly. This means that you should use `#all` and `#first`\ninstead of the other Capybara::Node::Matchers or Capybara::Node::Finders. All the methods provided\nby AePageObjects::ElementProxy are safe as well. All the necessary waiting / rescuing is handled by\nthe caller of the block.\n\n### Windows\n\nEvery document exists within a browser window. The `window` attribute of a `AePageObject::Document` provides access to\nthe window hosting the document.\n\n### Multiple Windows\n_only works when using Selenium::WebDriver_\n\nSometimes websites launch documents in new windows or tabs. To find a document in another window use `browser.find_document`:\n\n```ruby\n  def show_report!(report_name)\n    node.click_on(\"Show #{report_name}\")\n\n    browser.find_document(ReportPage)\n  end\n```\n\n`browser.find_document` can be parameterized with a block to refine the search criteria:\n\n```ruby\n  def show_report!(report_name)\n    node.click_on(\"Show #{report_name}\")\n\n    browser.find_document(ReportPage) do |report|\n      report.filters.date.text == Time.now.to_date\n    end\n  end\n```\n\n### Conventions\n\nA few conventions have evolved to aid in writing maintainable page objects and test code using page objects. Methods\ncausing the browser to navigate to a new page should:\n\n1. Be ! methods\n2. Return a handle to the resulting page by either:\n - calling `window.change_to`, OR\n - calling `browser.find_document`\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_user_session\n\n  def login!(username, password)\n    email.set username\n    password.set password\n\n    node.click_on(\"Log In\")\n\n    window.change_to(AuthorsIndexPage)\n  end\nend\n\nclass AuthorsIndexPage \u003c AePageObjects::Document\n  def show_report!(report_name)\n    node.click_on(\"Show #{report_name}\")\n\n    browser.find_document(ReportPage)\n  end\nend\n```\n\nSome test code using these page objects:\n\n```ruby\ndef test_logging_in_goes_to_authors\n  login_page = LoginPage.visit\n\n  authors_page = login_page.login!('admin', 'password')\n  assert_equal AuthorsIndexPage, authors_page.class\n\n  books_page = authors_page.show_report!(\"Book Report\")\n  assert_equal ReportPage, books_page.class\nend\n```\n\nKeeping the conventions in mind while reading the above test code should make it clear to the reader that the login!\nmethod will be navigating the browser to a new page within the current window; any references to the previous page will\nbe invalid. Accessing the login_page reference after the browser has changed pages will result in an\n`AePageObjects::StalePageObject` error:\n\n\n```ruby\ndef test_logging_in_goes_to_authors\n  login_page = LoginPage.visit\n  authors_page = login_page.login!('admin', 'password')\n\n  login_page.email.text\n\n  # above line raises:\n  # AePageObjects::StalePageObject: Can't access stale page object '#\u003cLoginPage:0x11c604268\u003e'\nend\n```\n\nThe same is true for the show_report!() method: the report page will open up in a new window, and the caller needs to\nuse the returned handle to this page.\n\n#### Variable Results\n\nOftentimes the page that results from a form submission is based on the data entered into the form. This makes following\nconvention #2 difficult. Additionally, the test code that is entering data into the form has the knowledge to know which\npage should result. Both `window.change_to` and `browser.find_document` handle this case by accepting the set of all\npossible pages that can result:\n\n\n```ruby\ndef login!(username, password)\n  email.set username\n  password.set password\n\n  node.click_on(\"Log In\")\n\n  window.change_to(AuthorsIndexPage, LoginPage, DashboardPage)\nend\n\ndef show_report!(report_name)\n  node.click_on(\"Show #{report_name}\")\n\n  browser.find_document(ReportPage, DashboardPage)\nend\n\n```\n\nIn both cases (`window.change_to` or `browser.find_document`) will return a handle to a document matching the parameter\n set: `window.change_to` will only look in the current window while `browser.find_document` will look across all open\n windows. The first parameter to these methods is considered the default page.\n\nCode calling the login!() method can inspect the resultant page before proceeding:\n\n```ruby\nresult = LoginPage.visit.login!(\"username\", \"invalid password\")\nassert result.is_a?(AuthorsIndexPage)\n\nauthor_page = result\nauthor_page.first_name.set \"New Name\"\n...\n```\n\nAlternatively, the calling code can use `as_a`:\n\n```ruby\nauthor_page = LoginPage.visit.login!(\"username\", \"invalid password\").as_a(AuthorsIndexPage)\nauthor_page.first_name.set \"New Name\"\n...\n```\n\n`as_a` will fail with `AePageObjects::DocumentLoadError` if the page in the browser is not of the specified type.\n\nWhen `as_a` is not used, an internal implicit cast is made to the default page which ensures that the page is of the\ndefault document type (the first document specified through the parameters of `window.change_to` or\n`browser.find_document`). If the check fails, a `AePageObjects::DocumentLoadError` will raise.\n\n\n## Elements\n\nElements in AePageObjects represent the DOM elements on a page and are subclasses of `AePageObject::Element`. Just like\nin Capybara, all elements have a reference to their parent element. The parent of the topmost element in the element tree\nis `AePageObject::Document`.\n\n### Defining Elements\n\nAePageObjects provides a concise DSL for defining elements on a document. Elements defined on a document express the static\nstructure of your page.\n\nFor example:\n\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :first_name\n  element :last_name\nend\n```\n\nThe above use of `element` defines two elements on the page (first_name, and last_name). The elements can be accessed\non an instance of AuthorsShowPage:\n\n```ruby\nauthor_page = AuthorsShowPage.new\nauthor_page.first_name #-\u003e #\u003cAePageObjects::Element:0x11cec0280\u003e@name:\u003cfirst_name\u003e\u003e\nauthor_page.last_name  #-\u003e #\u003cAePageObjects::Element:0x11cec0346\u003e@name:\u003clast_name\u003e\u003e\n```\n\nThe methods defined by `element` return instances of `AePageObjects::Element`. When calling these methods AePageObjects\nwill initialize instances of `AePageObjects::Element` with the underlying `Capybara::Node::Element` matching the element.\nHow AePageObjects goes about finding the underlying `Capybara::Node::Element` is described by a locator ([Locators](#locators)).\n\nBy default AePageObjects will look for DOM elements with ids matching the element's names (so, #first_name and #last_name in\nthis case). Using the `element` method you can specify a different name to be used for locating the element:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :first_name\n  element :last_name, name: 'sur_name'\nend\n```\n\nWith the definition above, accessing `last_name` will cause AePageObjects to look for an element with id 'sur_name' instead\nof 'last_name'.\n\nIf you need more control of how the element is located on the page you can specify a locator:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :first_name\n  element :last_name, locator: [:css, '.last_name', {visible: true}]\nend\n```\n\nSee [Locators](#locators) for a discussion of locators.\n\n### Creating elements on the fly\n\nIn addition to defining elements to express the static structure of a page, you can also create elements on the fly by\ncalling the `element` method on a node:\n\n```ruby\nsome_modal = some_page.element('.dialog')\nclose_button = some_modal.element('.x-close')\n```\n\n`element` will return a new `AePageObjects::Element` object with a `parent` pointer to the object `element` was called\non.\n\nThis is useful for designing the page object interface for things like modals which cannot be interacted with until\nthey are triggered by some action (e.g. a button click). Instead of declaring the modal as a static element of the\nDocument, you would define a method that corresponds to the action that opens the modal, and this method would yield\nthe dynamically created element. For example, instead of writing something like:\n\n```ruby\nclass ShowPage \u003c AePageObjects::Document\n  element :share_modal, is: ShareModal # bad because the `share_modal` element\n  # can always be accessed, even when the real modal cannot be interacted with\n\n  def share_image(\u0026block)\n    node.click_button('Share')\n    share_modal.wait_until_visible\n    block.call(share_modal)\n    share_modal.wait_until_hidden\n  end\nend\n```\n\nyou'd write:\n\n```ruby\nclass ShowPage \u003c AePageObjects::Document\n  def share_image(\u0026block)\n    node.click_button('Share')\n    share_modal = element(locator: '#share_modal', is: ShareModal)\n    share_modal.wait_until_visible\n    block.call(share_modal)\n    share_modal.wait_until_hidden\n  end\nend\n```\n\nwhich only exposes the `share_image` method on instances of ShowPage (it does not expose a `share_modal` method). Thus,\nusers can only access a `share_modal` element in the block this method yields to, which is the only time the modal can\n*actually* be interacted with.\n\n### Nested Elements\n\nThe `element` method can take a block to define nested elements:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :address do\n    element :street\n    element :city\n  end\nend\n```\n\nThe above definition describes a DOM structure like the following:\n\n```html\n\u003cdiv id=\"address\"\u003e\n  \u003cdiv id=\"address_street\"\u003e\u003c/div\u003e\n  \u003cdiv id=\"address_city\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\nAll instances of `AePageObjects::Element` have a reference to the parent node ([Elements](#elements)). In addition to\n`name` all elements have a `full_name` which is determined by walking the parent reference list all the way up to the\n document and joining the names of the elements along the way with underscore. For example:\n\n```ruby\nauthor_page = AuthorsShowPage.new\nauthor_page.address.full_name         #-\u003e 'address'\nauthor_page.address.name              #-\u003e 'address'\n\nauthor_page.address.street.full_name  #-\u003e 'address_street'\nauthor_page.address.street.name       #-\u003e 'street'\n\nauthor_page.address.city.full_name    #-\u003e 'address_city'\nauthor_page.address.city.name         #-\u003e 'city'\n```\n\nThe ids used for the nested elements (street and city) include the name of the containing element (address). Just like\nin the previous examples, you can change the names for any of the elements to match your HTML. For example, this:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :address, name: 'primary_address' do\n    element :street, name: 'street1'\n    element :city\n  end\nend\n```\n\n...which results in:\n\n```ruby\nauthor_page = AuthorsShowPage.new\nauthor_page.address.full_name         #-\u003e 'primary_address'\nauthor_page.address.name              #-\u003e 'primary_address'\n\nauthor_page.address.street.full_name  #-\u003e 'primary_address_street1'\nauthor_page.address.street.name       #-\u003e 'street1'\n\nauthor_page.address.city.full_name    #-\u003e 'primary_address_city'\nauthor_page.address.city.name         #-\u003e 'city'\n```\n\n...and will look for a DOM structure like:\n\n```html\n\u003cdiv id=\"primary_address\"\u003e\n  \u003cdiv id=\"primary_address_street1\"\u003e\u003c/div\u003e\n  \u003cdiv id=\"primary_address_city\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\nNotice that the id used to find each element matches the `full_name` of the element. This is because the default locator\nuses the `full_name` (see [Default Locator](#default-locator)).\n\nElements can be nested recursively forever...\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :contact_info do\n    element :name\n    element :phone_number\n\n    element :address, name: 'primary_address' do\n      element :street, name: 'street1'\n      element :city\n    end\n  end\nend\n```\n\n#### Extending Nested Elements\n\nYou can add custom behavior to nested elements by manipulating the nested element's class directly from within the block.\nThe block passed to `element` is instance_eval'd within the context of a one-off subclass of `AePageObjects::Element`.\nAnything you can do to a class, you can do inside of this block.\n\n```ruby\nmodule Toggleable\n  def toggle(times)\n    times.times do\n      hide\n      show\n    end\n  end\nend\n\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :address do\n    include Toggleable\n\n    element :street\n    element :city\n\n    def hide\n      node.find('.hide-button').click\n    end\n\n    def show\n      node.find('.show-button').click\n    end\n  end\nend\n\nauthor_page = AuthorsShowPage.new\n\n# toggle the address\nauthor_page.address.hide()\nauthor_page.address.show()\n\nauthor_page.address.toggle(7)\n```\n\n### Custom Elements\n\nConsider:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :contact_info do\n    element :name\n    element :phone_number\n\n    element :address do\n      element :street\n      element :city\n    end\n  end\nend\n\nclass BusinessShowPage \u003c AePageObjects::Document\n  element :address do\n    element :street\n    element :city\n  end\nend\n```\n\nThe `address` element in each of these pages uses the exact same structure. Specifying the `:is` option to the element\nDSL can be used to reuse common element types. The above rewritten using `:is`:\n\n```ruby\nclass Address \u003c AePageObjects::Element\n  element :street\n  element :city\nend\n\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :contact_info do\n    element :name\n    element :phone_number\n\n    element :address, is: Address\n  end\nend\n\nclass BusinessShowPage \u003c AePageObjects::Document\n  element :address, is: Address\nend\n```\n\nThis is particularly useful when crafting page objects to interact with Rails' partials.\n\nAdditionally, `:is` can be used for creating custom element types:\n\n```ruby\nclass ThreePartDate \u003c AePageObjects::Element\n  element :month\n  element :day\n  element :year\n\n  def value\n    Date.new(year.value, month.value, day.value)\n  end\nend\n\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :birth_date, is: ThreePartDate\nend\n\nauthor_page = AuthorsShowPage.new\nauthor_page.birth_date.value # -\u003e \"1988-04-01\"\n```\n\n### Forms\n\n`AePageObjects::Form` is a special type of `AePageObjects::Element` for working with forms. The `form_for` DSL method\ncan be used to define a form and is a special case of the `element` DSL method.\n\n```ruby\nclass AuthorsNewPage \u003c AePageObjects::Document\n  form_for :author do\n    element :first_name\n    element :last_name\n  end\nend\n```\n...which results in:\n\n```ruby\nnew_author_page = AuthorsNewPage.new\nnew_author_page.author                      #-\u003e #\u003cAePageObjects::Form:0x11cec0280\u003e@name:\u003cauthor\u003e\u003e\nnew_author_page.author.name                 #-\u003e 'author'\nnew_author_page.author.full_name            #-\u003e 'author'\n\nnew_author_page.first_name                  #-\u003e #\u003cAePageObjects::Element:0x11cec0567\u003e@name:\u003cfirst_name\u003e\u003e\nnew_author_page.author.first_name           #-\u003e #\u003cAePageObjects::Element:0x11cec1537\u003e@name:\u003cfirst_name\u003e\u003e\nnew_author_page.author.first_name.name      #-\u003e 'first_name'\nnew_author_page.author.first_name.full_name #-\u003e 'author_first_name'\n\nnew_author_page.last_name                   #-\u003e #\u003cAePageObjects::Element:0x11cec0876\u003e@name:\u003clast_name\u003e\u003e\nnew_author_page.author.last_name            #-\u003e #\u003cAePageObjects::Element:0x11cec3452\u003e@name:\u003clast_name\u003e\u003e\nnew_author_page.author.last_name.name       #-\u003e 'last_name'\nnew_author_page.author.last_name.full_name  #-\u003e 'author_last_name'\n```\n\nNotice: the nested elements (first_name and last_name) are accessible on the new_author_page.\n\nThe above expects a DOM structure like:\n\n```html\n\u003cform id=\"author\"\u003e\n  \u003cinput id=\"author_first_name\" /\u003e\n  \u003cinput id=\"author_last_name\" /\u003e\n\u003c/form\u003e\n```\n\n### Collections\n\nAePageObject::Collection is used to describe repeated structured data on the page.\n\n```ruby\nclass AuthorsNewPage \u003c AePageObjects::Document\n  form_for :author do\n    collection :addresses do\n      element :street\n      element :city\n    end\n  end\nend\n```\n\n...and will look for a DOM structure like:\n\n```html\n\u003cdiv id=\"addresses\"\u003e\n  \u003cdiv class=\"address\"\u003e\n    \u003cinput id=\"author_addresses_0_street\" name=\"addresses[0][street]\" /\u003e\n    \u003cinput id=\"author_addresses_0_city\" name=\"addresses[0][city]\" /\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"address\"\u003e\n    \u003cinput id=\"author_addresses_1_street\" name=\"addresses[1][street]\" /\u003e\n    \u003cinput id=\"author_addresses_1_city\" name=\"addresses[1][city]\" /\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\n...which results in:\n\n```ruby\nnew_author_page = AuthorsNewPage.new\nnew_author_page.addresses                   #-\u003e #\u003cAePageObjects::Collection:0x11cec0567\u003e@name:\u003caddresses\u003e\u003e\nnew_author_page.addresses.size              #-\u003e 2\nnew_author_page.addresses.first.street      #-\u003e #\u003cAePageObjects::Element:0x11cec0567\u003e@name:\u003cstreet\u003e\u003e\nnew_author_page.addresses.first.street.full_name #-\u003e 'author_addresses_0_street'\nnew_author_page.addresses.last.street.full_name #-\u003e 'author_addresses_1_street'\n```\n\nThe block passed to `collection` is a bit different than the block passed to `element`. With `element` the block defines\nnested elements. With `collection` the block defines the structure of each item in the collection. In place of the block\nyou can pass `:contains`. The following is equivalent to the above:\n\n```ruby\nclass Address \u003c AePageObjects::Element\n  element :street\n  element :city\nend\n\nclass AuthorsNewPage \u003c AePageObjects::Document\n  form_for :author do\n    collection :addresses, contains: Address\n  end\nend\n```\n\n\n#### Custom Collections\n\nSometimes, we'll want a collection that supports more methods\nthan what the [default implementation](./lib/ae_page_objects/elements/collection.rb)\nsupports. An easy way to handle this use case is to create a new\nclass that inherits from `AePageObjects::Collection`:\n\n```ruby\nclass Address \u003c AePageObjects::Element\n  element :street\n  element :city\nend\n\nclass AddressList \u003c AePageObjects::Collection\n  def delete_last\n    last.click('.delete-button')\n  end\nend\n```\n\nThen, in the page object, use `:is` to specify the collection type:\n\n```ruby\nclass AuthorsNewPage \u003c AePageObjects::Document\n  form_for :author do\n    collection :addresses, is: AddressList, contains: Address\n  end\nend\n```\n\nThis custom collection is also declared to contain items of the custom\n`Address` element subclass. `collection` supports every combination of\n`:is`, `:contains`, and the block. See the source for more examples.\n\n### Staling\n\nSometimes an element only exists on a page temporarily. In such cases, it's a good practice to stale the instance of the\nelement when it can no longer be interacted with:\n\n```ruby\nclass AlertBox \u003c AePageObjects::Element\n  def ok!\n    click_on(\"Ok\")\n    stale!\n  end\n\n  def close!\n    click_on(\"X\")\n    stale!\n  end\nend\n\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :alert, is: AlertBox\n  element :delete_button, locator: '.delete'\nend\n\ndef test_logging_in_goes_to_authors\n  authors_page = AuthorsShowPage.visit\n  authors_page.delete_button.click\n\n  alert_box = authors_page.alert\n  alert_box.ok!\n\n  alert_box.close!\n\n  # above line raises:\n  # AePageObjects::StalePageObject: Can't access stale page object '#\u003cAlertBox:0x11c604268\u003e'\nend\n```\n\n### Load Ensuring\n\nThe load ensuring mechanism for elements is the same as for documents ([Load Ensuring](#load-ensuring)) just without the URL check.\n\n### Checking presence\n\nUse ```present?``` and ```absent?``` to check the presence of an element on the page:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :delete_button, locator: '.delete'\nend\n\ndef test_delete_button_presence\n  authors_page = AuthorsShowPage.visit\n\n  assert authors_page.delete_button.present?\n  assert ! authors_page.delete_button.absent?\nend\n```\n\n### Waiting for presence\n\nUse ```wait_until_present``` and ```wait_until_absent``` to wait on an element's presence or absence within the page:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  def view_headshots(\u0026block)\n    node.click_link(\"View Headshots\")\n\n    viewer = element(locator: '#head-shots', is: HeadshotViewer)\n    viewer.wait_until_present(10) # wait 10 seconds\n\n    yield viewer\n\n    viewer.wait_until_absent\n  end\nend\n\ndef test_headshots\n  authors_page = AuthorsShowPage.visit\n  authors_page.view_headshots do |viewer|\n    assert_equal 0, viewer.shots.size\n  end\nend\n```\n\n### Checking visibility\n\nUse ```visible?``` and ```hidden?``` to check whether an element is present and visible on the page:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :delete_button, locator: '.delete'\nend\n\ndef test_delete_button_visibility\n  authors_page = AuthorsShowPage.visit\n\n  assert authors_page.delete_button.visible?\n  assert ! authors_page.delete_button.hidden?\nend\n```\n\n### Waiting for visibility\n\nUse ```wait_until_visible``` and ```wait_until_hidden``` to wait on an element's visibility:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :survey\nend\n\ndef test_survey\n  authors_page = AuthorsShowPage.visit\n\n  survey = authors_page.survey\n  survey.wait_until_visible\n  survey.dismiss!\n  survey.wait_until_hidden\nend\n```\n\n### Locators\n\nLocators in AePageObjects are used to find elements on a page and are expressions of how to locate an element from within\nthe context of an existing node. Anything that `Capybara::Node::Base::find` supports as arguments can be used as a locator:\n\n```ruby\n[:css, '.somecss #selector']                      #-\u003e calls \u003ccapybara-node\u003e.find(:css, '.somecss #selector')\n'.somecss #selector'                              #-\u003e calls \u003ccapybara-node\u003e.find('.somecss #selector')\n[:xpath, '//div/tr']                              #-\u003e calls \u003ccapybara-node\u003e.find(:xpath, '//div/tr')\n['.somecss #selector', {visible: true}]        #-\u003e calls \u003ccapybara-node\u003e.find('.somecss #selector', visible: true)\n```\n\n`Capybara::Node::Base::find` finds elements from within the context of the element `find` is called on. The same is true\nfor AePageObjects locators. For example, given this DOM structure:\n\n```html\n\u003cdiv id=\"div1\"\u003e\n  \u003cdiv class=\"highlight\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv id=\"div2\"\u003e\n  \u003cdiv class=\"highlight\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\n..and this locator:\n\n```ruby\n[:css, '.highlight']\n```\n\nThe node found depends on the context of the existing node. If this locator is used within the context of `div#div1` then\nthe element at `div#div1 .highlight` will be found. If this locator is used within the context of `div#div2` then the\nelement at `div#div2 .highlight` will be found.\n\nIn addition to the valid argument types to `Capybara::Node::Base::find`, locators can also be procs:\n\n```ruby\nproc { [:css, \".somecss ##{self.name}\", {visible: true}] }\n```\n\nLocators that are procs are instance_eval'd within the context of the existing `AePageObject::Node`.\n\nFor example:\n\n```ruby\nclass AuthorsShowPage \u003c AePageObjects::Document\n  element :first_name, locator: proc { [:xpath, \"//*[contains(@id, '#{name}')]\"] }\nend\n\nauthor_page = AuthorsShowPage.new\nauthor_page.first_name            #-\u003e calls \u003ccapybara-node\u003e.find(:xpath, \"//*[contains(@id, 'first_name')]\")\n```\n\n#### Default Locator\nBy default, all instances of `AePageObjects::Element` will use the following locator:\n\n\n```ruby\nproc { \"##{__full_name__}\" }\n```\n\n## Router\n\nThe router reads the path specifications on documents and navigates the browser appropriately\n(see [Document Navigation](#navigation) for details).\n\nWhen using in a Rails project, the default router understands Rails' named routes. Consider:\n\n```ruby\nclass LoginPage \u003c AePageObjects::Document\n  path :new_session\nend\n\nLoginPage.visit\n```\n\nIn the above example, `visit` will use the router to determine the URL to use to navigate the browser. The router will\ninvoke `new_session_path` on the router of the Rails application to determine the URL.\n\n### Configuration\n\nThe router can be changed either globally or on a per document basis.\n\n#### Router interface\n\nRouters must implement the following interface:\n\n```ruby\nclass MyFavRouter\n  # returns true if the path specification recognizes the url\n  def path_recognizes_url?(path, url)\n  end\n\n  # returns a string representing the url\n  def generate_path(path, *args)\n  end\nend\n```\n\nThe `path` parameter holds the document path specification (`new_session` in the above example). The `url` argument is\nthe current URL of the browser window. The `args` parameter holds the arguments passed to `visit`.\n\n#### Configure default router\n\nTo change the router globally set `AePageObjects.default_router` to an object that implements the router interface:\n\n```ruby\nAePageObjects.default_router = MyFavRouter.new\n```\n\n#### Configure router per document\n\nTo change the router on a per document basis set the `router` property on the document class directly:\n\n```ruby\nLoginPage.router = MyFavRouter.new\n```\n\n### Sharing routers across groups of documents\n\nIn complex applications, it may be necessary to use multiple routers for different groups of documents.\nTo accomplish this, group document classes under base classes and set the router on the base class.\n\nFor example:\n\n```ruby\nclass AdminDocument \u003c AePageObjects::Document\n  self.router = AdminRouter.new\nend\n\nclass AdminSettingsPage \u003c AdminDocument\n  path :admin_settings\nend\n\nclass ReportDocument \u003c AePageObjects::Document\n  self.router = ReportingRouter.new\nend\n\nclass UsageReportPage \u003c ReportDocument\n  path :usage_report\nend\n\nclass LoginPage \u003c AePageObjects::Document\n  path :new_session\nend\n```\n\n`AdminSettingsPage.visit` will use `AdminRouter`.\n`UsageReportPage.visit` will use `ReportingRouter`.\n`LoginPage.visit` will use `AePageObjects.default_router`.\n\n## Development\n\nTo set up a development environment and run the basic unit tests:\n\n```\nbundle install\nbundle exec rake\n```\n\nSee the [Development documentation](development.md) for more detailed information.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappfolio%2Fae_page_objects","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fappfolio%2Fae_page_objects","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappfolio%2Fae_page_objects/lists"}