EDSL - Element DSL
This gem implements an extensible DSL for declaring web elements as part of a page object pattern. This gem does not implement the page object pattern, for an implementation of page object using this gem see edsl-pageobject.
This gem was created out of the need to rapidly produce abstractions for non-standard web elements. With EDSL support for new accessors and other DSL methods can be implemented without needing to modify any EDSL source or monkey patching. It allows you to rapidly prototype using lambdas, or to build full classes to use in place of existing elements.
Accessors
The DSL includes accessors for all Watir elements and makes it easy to add custom accessors. The accessors for Watir elements are not hand crafted ruby code, they're generated by code that extends EDSL when edsl/watir_elements
is included.
Below are the two lines of code that implement the support for buttons and links:
CLICKABLE_ELEMENTS = %i[button a link].freeze
CLICKABLE_ELEMENTS.each { |tag| EDSL.define_accessor(tag, how: tag, default_method: :click) }
EDSL.define_accessor
is the quickest way to add a new accessor. With it you provide a name for your accessor (here we're just using the name of the tag as the name of the accessor) as well as the default set of options to be used in the call to element
.
Most accessors can be implemented as a custom call to element
in the DSL. Depending on the options passed to element
you can retrieve any in the DOM. Filling out all those options for every element you wanted to locate would be tedious, so define_accessor
will define a new method that takes the default options provided, merges them with the ones provided by the developer using the new accessor and then calls element
.
The options hash for element
can contain the following keys:
-
how - The method to call, or a proc to call to locate this element. i.e.
:div
-
default_method - The method to call when
name
is called. If not provided it will be the same as callingname_element
-
assign_method - The method to call when
name=
is called. If not set thename=
method will not be created. -
presence_method - The method to call when
name?
is called. If not provided it will default to:present?
-
hooks - CptHook::HookDefintions to apply to the element before returning it in
name_element
. If not supplied, hooks are not applied. - wrapper_fn - Optional, can be set to a proc that will be called with the Watir element and it's parent container as arguments.
Anything left in the hash after edsl options are deleted are passed to :how
These options allow for several possibilities, for example we could create a new span_button
for spans that are clickable like buttons with:
EDSL.define_accessor(:span_button, how: span, default_method: :click)
For more complex needs one can use the EDSL.extend_dsl
method and define new DSL methods directly. Here's an example from edsl-pageobject where an additional parameter was needed.
EDSL.extend_dsl do
def section(name, section_class, opts)
element(name, { how: :div, assign_method: :populate_with,
wrapper_fn: lambda { |element, _container|section_class.new(element, self) } }.merge(opts))
end
end
This makes use of the :wrapper_fn
option to create a new instance of the provided section class with the actual element as it's root and return that when asked for name
or name_element
.
Consuming accessors
In order to make use of accessors you need to include the EDSL module into a class. EDSL assumes that the class including it respond to any of the methods provided in the :how
option. The easy way to do that is to inherit from EDSL::ElementContainer
. The key piece of "magic" in it is that it inherits from SimpleDelegator
and can be initialized with a Watir element or browser.
Most accessors have a signature of accessor(name, opts)
where name
determines the method names generated and opts
provide options. At a minimum opts
should include the locators for the element. For Watir accessors the options are the same as the corresponding Watir calls.
First a simple example using one of the Watir accessors
class LoginPage < EDSL::ElementContainer
text_field(:username, id: 'user_name')
end
# Now we could do:
page = LoginPage.new(browser)
page.username? # See if the text field exists
page.username = 'zerocool` # Set the username
name = page.username # Get the username
username_edit = page.username_element # The the underlying Watir element
Due to the way DSL extensions work, it is possible to override behavior at the top level.
For example, let's say for some link what we care about is where it points, not visiting it. The default method for a link would be to call click
forcing us to use page.link_element.href
to get what we're after when a more page.link
or page.link_href
would make it easier to compare with values from a fixture. We can accomplish this when we declare the link like so:
link(:result_link, index: 0, default_method: :href)
We can even find our own elements
By supplying a block to any accessor that block will be called to locate the element.
link(:result_link, default_method: :href) { some_variable? : link(index: 0) ? link(index: 1) }
We can customize value types
Let's say we had a text field for entering dates we want to be able to take advantage of Chronic and make our lives easier. We can define a couple lambda functions provide those in the options.
# Return a date, either by parsing a string or returning the passed value back
v_to_d = lambda { |val| val.is_a?(String) ? Chronic.parse(val) : val }
# Set an element based on a date, or a string Chronic can parse to a date
DATE_FORMAT = '%m/%d/%Y'.freeze # Americans ammiright?
chronic_set = lambda { |name, container, value| container.send("#{name}_element").set(v_to_d.call(value).strftime(DATE_FORMAT)) }
# Return a date from the value in the element
chronic_get = lambda { |name, container| Chronic.parse(container.send("#{name}_element").value) }
text_field(:date_input, id: 'the_date', assign_method: chronic_set, default_method: chronic_get )
# Now we can do fun things like
page.date_input = 'next Tuesday'
Another way we could accomplish the same task is to provide a wrapper function that wraps the element in a decorator to modify the behavior like so:
class DateEdit < SimpleDelegatopr
def value
Chronic.parse(super)
end
def set(val)
super(val_to_date(val).strftime(DATE_FORMAT))
end
def val_to_date(val)
val.is_a?(String) ? Chronic.parse(val) : val
end
end
text_field(:date_input, id: 'the_date',
wrapper_fn: lambda { |ele, _parent| DateEdit.new(ele) } )
Installation
Add this line to your application's Gemfile:
gem 'edsl'
And then execute:
$ bundle
Or install it yourself as:
$ gem install edsl
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/edsl. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Edsl project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.