Skip to content

Donavan/edsl

Repository files navigation

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 calling name_element
  • assign_method - The method to call when name= is called. If not set the name= 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.

Releases

No releases published

Packages

No packages published