Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/bwilczek/webdriver_pump
Page Object Model library for Crystal. A port (kind of) of Ruby's WatirPump
https://github.com/bwilczek/webdriver_pump
Last synced: 2 days ago
JSON representation
Page Object Model library for Crystal. A port (kind of) of Ruby's WatirPump
- Host: GitHub
- URL: https://github.com/bwilczek/webdriver_pump
- Owner: bwilczek
- License: mit
- Created: 2018-12-27T17:51:12.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2021-06-12T08:20:50.000Z (over 3 years ago)
- Last Synced: 2024-10-03T12:26:04.508Z (2 months ago)
- Language: Crystal
- Size: 92.8 KB
- Stars: 4
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-crystal - webdriver_pump - Page Object library. Inspired by Ruby's [WatirPump](https://github.com/bwilczek/watir_pump) (Testing)
- awesome-crystal - webdriver_pump - Page Object library. Inspired by Ruby's [WatirPump](https://github.com/bwilczek/watir_pump) (Testing)
README
# webdriver_pump
![Build Status](https://github.com/bwilczek/webdriver_pump/actions/workflows/spec.yml/badge.svg?branch=master)
This shard is a Page Object Model lib, built on top of [selenium.cr](https://github.com/matthewmcgarvey/selenium.cr).
It's a crystal port of ruby's [watir_pump](https://github.com/bwilczek/watir_pump). Heavily inspired by [SitePrism](https://github.com/site-prism/site_prism).## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
webdriver_pump:
github: bwilczek/webdriver_pump
```
2. Run `shards install`## Basic usage
```crystal
require "webdriver_pump"# define the page model, for example:
class GreeterPage < WebdriverPump::Page
url "https://bwilczek.github.io/watir_pump_tutorial/greeter.html"
element :header, { action: :text, locator: -> { root.find_element(:xpath, "//h1") } }
element :fill_name, { action: :send_keys, locator: {id: "name"} }
element :submit, { action: :click, locator: {id: "set_name"} }
element :greeting, { action: :text, locator: {id: "greeting"} }
end# use it in specs, for example:
# where `session` is an instance of Selenium::Session
describe "Page without components" do
it "operates on Selenium::Elements" do
GreeterPage.new(session).open do |p|
p.header.should eq "Greeter app"
p.fill_name "Crystal"
p.submit
p.greeting.should eq "Hello Crystal!"
end
end
end
```## Documentation
Please refer to this chapter to learn how to define your Page Objects, and use it from your tests.
For complete code documentation please visit project's [GitHub Pages](https://bwilczek.github.io/webdriver_pump/).
### Overview
`WebdriverPump` provides a DSL (implemented as macros) to describe the Page Object Model.
It's a very close port of Ruby's `WatirPump` gem. There are some subtle differences in the implementation,
but the core concepts remain the same:* **Nestable, reusable components**, to build elegant APIs
* **Element actions**, to automatically generate simple, one liner methods (like wrappers for `click`)
* **Page scoping**, so that it's immediately known what `page` is currently being tested
* **Form helpers**, to operate HTML form elements with ease (WebDriver doesn't deliver here)
* **Decorated collections**, to access element/component collections with descriptive keys### Page
Describes the page under test. Inherits from `Component`, so please familiarize yourself
with that class as well to fully understand what `Page` is capable of. This section covers
only the differences from the base `Component` class.#### `url` macro
Declares the full URL to the page under test.
```crystal
class GitHubUserPage < WebdriverPump::Page
url "https://github.com/bwilczek"
enddescribe "Page's URL" do
it "navigates to given URL" do
GitHubUserPage.new(session).open do |p|
session.url.should eq "https://github.com/bwilczek"
end
end
end
```URL can be parameterized:
```crystal
class GitHubUserPage < WebdriverPump::Page
url "https://github.com/{user}"
enddescribe "Page's URL" do
it "navigates to given parameterized URL" do
GitHubUserPage.new(session).open(params: {user: "bwilczek"}, query: {repo: "webdriver_pump""}) do |p|
session.url.should eq "https://github.com/bwilczek?repo=webdriver_pump"
end
end
end
```#### `#open`
Navigates to Page's URL and executes given block in the scope of the page.
```crystal
class GitHubUserPage < WebdriverPump::Page
url "https://github.com/bwilczek"
element :user_nickname, { locator: { xpath:, "//span[@itemprop='additionalName']") } }
enddescribe "Page's URL" do
it "navigates to given URL" do
GitHubUserPage.new(session).open do |page|
page.class.should eq GitHubUserPage
page.user_nickname.class.should eq Selenium::Element
page.user_nickname.text.should eq "bwilczek"
end
end
end
```#### `#use`
Similar to `#open`, but does not perform the navigation - assumes that the page is already open.
Useful when the navigation is triggered by an action on a different page.```crystal
GitHubUserPage.new(session).open do { |p| p.navigate_to_repo("webdriver_pump") }GitHubRepoPage.new(session).use do |p|
p.class.should eq GitHubRepoPage
end
```#### `#loaded?`
Predicate method denoting if page is ready to be interacted with.
In most cases creation of this method will not be required, since `WebDriver` itself
checks if page's resources have been loaded. Only in case of more complex pages,
that heavily rely on parts loaded dynamically over XHR providing of custom `loaded?`
criteria might be necessary.```crystal
class GitHubUserPage < WebdriverPump::Page
url "https://js-heavy.com"
element :created_by_xhr, { locator: {id: "content"} }def loaded?
created_by_xhr.displayed?
end
end
```### Component
Components are the foundation of `WebdriverPump` models.
They abstract out certain sub-trees of the page's DOM tree into `crystal` classes
and hide the underlying HTML behind the business oriented API.`Pages` are the top-level components, that abstract out the complete page (DOM sub-tree starting at `//body`).
Components can be nested, and grouped into collections.
They are declared inside their parent components using `element` macro, with a `class` parameter, that refers to `crystal` class, a child of `WebdriverPump::Component` (NOT a CSS class).
#### `#initialize` (constructor)
Usually invoked implicitly by the `element(s)` macro.
Accepts two parameters:
* `@session : Selenium::Session`
* `@root : Selenium::Element`Example of explicit usage:
```crystal
class OrderItemDetails < WebdriverPump::Component
# omitted for brevity
endclass OrderPage < WebdriverPump::Page
# omitted for brevitydef [](name)
node = root.find_element(:xpath, ".//div[@class='item' and contains(text(), '#{name}')]")
OrderItemDetails.new(session, node)
end
endOrderPage.new(session).open do |order|
order["Rubber hammer, 2kg"].class.should eq OrderItemDetails
end
```#### `#session`
Reference to associated `Selenium::Session` instance.
#### `#root`
Mounting point of current component in the DOM tree. Type: `Selenium::Element`.
For `Pages` it points to `//body`.
#### `#wait`
Reference to `WebdriverPump::Wait` module. Usage:
```crystal
# with default settings
wait.until { condition_is_met }# with custom settings
wait.until(timeout: 19, interval: 0.3) { other_condition_is_met }# global config (optional)
WebdriverPump::Wait.timeout = 10 # default = 15
WebdriverPump::Wait.interval = 0.5 # default = 0.2
```#### `element` macro
A DSL macro to declare `Element`s located inside given component.
```crystal
class MyPage < WebdriverPump::Page
url "http://example.org"# synopsis:
# element :name : Symbol, params : NamedTuple# examples
# locate and return Selenium::Element
element :title1, { locator: {xpath: ".//div[@role='title']"} }
# equivalent of:
def title1
root.find_element(:xpath, ".//div[@role='title']")
end# locate Selenium::Element and perform action (invoke method) on it at once
element :title2, { locator: {xpath: ".//div[@role='title']"}, action: :text }
# equivalent of:
def title2
root.find_element(:xpath, ".//div[@role='title']").text
end# locate Selenium::Element and use it as a mounting point for another component
element :title3, { locator: {xpath: ".//div[@class='user_details']"}, class: UserDetails }
# equivalent of:
def title3
node = root.find_element(:xpath, ".//div[@role='title']")
UserDetails.new(session, node)
end
end
```##### locator
Required parameter. Locator of the `Element` in the DOM tree. Allowed formats are:
* 1 element `NamedTuple` with key in (`:id`, `:name`, `:tag_name`, `:class_name`, `:css`, `:link_text`, `:partial_link_text`, `:xpath`), and a respective value.
* a `Proc` returning `Element`, e.g. `-> { root.find_element(:id, "user") }`, `-> { some_wrapper_element.find_element(:id, "user") }`##### action
`Symbol`, name of `Element`s method to be executed.
##### class
`Component` class. If provided the `Element` located using `locator` will be the mounting point for the component of given class.
#### `elements` macro
A DSL macro to declare a collection of `Element`s inside given component.
```crystal
class CollectionIndexedByName(T) < WebdriverPump::ComponentCollection(T)
def [](name)
ret = find { |el| el.name == name }
raise "Component with name='#{name}' not found" unless ret
ret
end
endclass OrderItem < WebdriverPump::Component
element :name, { action: :text, locator: {css: ".name"} }
endclass OrderPage < WebdriverPump::Page
elements :raw_order_items, { locator: {xpath: ".//li"} }elements :order_items, {
locator: {xpath: ".//li"},
class: OrderItem,
collection_class: CollectionIndexedByName(OrderItem)
}
endOrderPage.new(session).open do |page|
page.raw_order_items.class.should eq Array(Selenium::Element)
page.raw_order_items[0].class.should eq Selenium::Elementpage.order_items.class.should eq CollectionIndexedByName(OrderItem)
page.order_items["Rubber hammer, 2kg"].should eq OrderItem
end
```##### locator
Required parameter. Same rules as for `element` macro, but returns `Array(Element)`.
##### class
Optional `Component` class to wrap each of the collection's elements.
##### collection_class
Optional `ComponentCollection` class to wrap the whole collection. Useful to introduce more descriptive ways of accessing elements.
#### Form helper macros
`WebDriver` API itself does not provide methods to easily set and get values of HTML form elements.
This is where `WebdriverPump`'s form helper macros come handy.##### form_element
A macro that generates getter and setter methods for common HTML form elements.
```crystal
class LoginPage < WebdriverPump::Page
form_element :username, { class: TextField, locator: {name: "username"} }# iis equivalent of:
def username
# some logic that gets the value of given TextField
enddef username=(val)
# some logic that sets the value of given TextField to val
end
endLoginPage.new(session).open do |page|
page.username = "Bob"
page.username.should eq "Bob"
end
```The supported form elements are:
| FormElement | expected locator | value type | value |
|-----------------|------------------|---------------|---------------------------------------|
| TextField | ElementLocator | String | content of the input field |
| TextArea | ElementLocator | String | content of the text area |
| RadioGroup | ElementsLocator | String | label of the checked input element |
| CheckboxGroup | ElementsLocator | Array(String) | labels of the checked input elements |
| Checkbox | ElementLocator | Bool | label of the checked input element |
| SelectList | ElementLocator | String | label of the selected option element |
| MultiSelectList | ElementLocator | Array(String) | labels of the checked option elements |Please bear in mind that some of them require a locator for a single DOM node (e.g. `TextField` for a ``),
while other ones require a locator for multiple DOM nodes, that consist the form element (e.g. `CheckboxGroup` for a collection of ``).##### fill_form
This macro acts as a wrapper for calling multiple `form_element` setters at once.
Let's consider the following example:
```crystal
class LoginPage < WebdriverPump::Page
form_element :username, { class: TextField, locator: {name: "username"} }
form_element :password, { class: TextField, locator: {name: "password"} }
element :submit_form, { locator: {id: "submit"}, action: :click }fill_form :login, { submit: :submit_form, fields: [:username, :password] }
# equivalent of:
def login(params)
self.username = params[:username]
self.password = params[:password]
submit_form
end
end# Usage:
LoginPage.new(session).open do |page|
page.login(username: "bob", password: "secret")
end
````fill_form` macro expects the following parameters:
* `Symbol` name of the method to be generated
* `NamedTuple` with the following parameters
* `fields` - (required) `Array(Symbol)` - list of setters to be invoked
* `submit` - (optional) `Symbol` - name of the method to be executed after all setters##### form_data
This macro acts as a wrapper for calling multiple `form_element` getters at once.
It returns a `NamedTuple` with keys being the getter method names, and values the results that they return.Let's consider the following example:
```crystal
class SummaryPage < WebdriverPump::Page
form_element :title, { class: TextField, locator: {name: "title"} }# form_data doesn't require `form_element`s - it will work with all instance methods that don't require arguments
element :header { locator: {xpath: "../h1"}, action: :text }form_data :summary, { fields: [:title, :header] }
# equivalent of:
def summary
{
title: self.title,
header: self.header
}
end
end# Usage:
SummaryPage.new(session).open do |page|
summary = page.summary
summary[:title].should eq page.title
summary[:header].should eq page.header
end
````form_data` macro expects the following parameters:
* `Symbol` name of the method to be generated
* `NamedTuple` with the following parameters
* `fields` - (required) Array(Symbol) - list of methods to be invoked and their results returned## Development roadmap
- [x] Page without Components
- [x] Declare raw WebDriver elements with webdriver locators
- [x] Declare raw WebDriver elements with lambdas
- [x] Declare actions on WebDriver elements
- [x] Declare reusable Components
- [x] Collections of elements
- [x] Collections of Components
- [x] Nest Components
- [x] Wait for AJAX-driven Components to be ready to interact
- [x] ComponentCollection class
- [x] Fill in complex forms: RadioGroups, SelectLists
- [x] Parametrize Page url
- [x] Support `loaded?` predicate for Pages
- [x] Port WatirPump's form helpers
- [ ] Form helper for file upload (?)
- [x] Introduce Exception classes
- [x] Update README
- [ ] Update code documentation## Contributing
1. Fork it ()
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Test your changes, add relevant specs
4. Commit your changes (`git commit -am 'Add some feature'`)
5. Push to the branch (`git push origin my-new-feature`)
6. Create a new Pull Request## Contributors
- [Bartek Wilczek](https://github.com/bwilczek) - creator and maintainer